package cn.buk.tms.dao;

import cn.buk.common.sc.Page;
import cn.buk.tms.dto.AuthInfo;
import cn.buk.tms.factory.OperationLogFactory;
import cn.buk.tms.sc.SearchBillInfoCriteria;
import cn.buk.tms.sc.SearchFlightOrderCriteria;
import cn.buk.tms.sc.SearchReceiptCriteria;
import cn.buk.common.util.DateUtil;
import cn.buk.common.util.StringUtil;
import cn.buk.tms.entity.bill.*;
import cn.buk.tms.entity.misc.*;
import cn.buk.tms.entity.order.flight.FlightChangeOrder;
import cn.buk.tms.entity.order.flight.FlightOrder;
import cn.buk.tms.entity.order.flight.FlightOrderPassenger;
import cn.buk.tms.entity.order.flight.FlightRefundOrder;
import cn.buk.tms.entity.order.train.TrainOrder;
import cn.buk.tms.entity.order.vas.VasOrder;
import cn.buk.tms.entity.order.vas.VasOrderPassenger;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;

import jakarta.persistence.criteria.*;
import jakarta.transaction.Transactional;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

import static cn.buk.tms.entity.bill.BillInfo.CUSTOMER_PAYMENT_RECEIVED;
import static cn.buk.tms.entity.misc.TicketSale.*;
import static cn.buk.tms.common.constant.TmsOrderConstant.*;

/**
 * @author yfdai
 */
@Component
@Transactional
public class BillDaoImpl extends AbstractBillDao implements BillDao {

  private static final Logger logger = LogManager.getLogger(BillDaoImpl.class);

  @Override
  public BillInfo getBillInfo(int id) {
    return doGetBillInfo(id, true);
  }

  @Override
  public BillInfo getBillInfo(int id, boolean fetchChild) {
    return doGetBillInfo(id, fetchChild);
  }

  /**
   * 修改账单所属客户
   */
  @Override
  public int changeBillCustomer(final int enterpriseId, final String username, final int billId, final int customerId,
                                final String costCenter, final String projectName) {
    int retCode = 0;

    String remark;
    Customer customer = null;
    int customerType = 0;
    if (customerId == 0) {
      remark = "更改客户为：散客";
      retCode = em.createQuery("update BillInfo o set o.customer = null, o.customerType = 0, o.costCenter = null, o.projectName = null " +
              "where o.enterpriseId = :enterpriseId and o.id = :billId and (o.audited is null or o.audited <> :auditStatus) ")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billId", billId)
              .setParameter("auditStatus", "1")
              .executeUpdate();
    } else {
      customer = em.find(Customer.class, customerId);

      if (customer.getEnterpriseType() == Customer.ENT_INDIVIDUAL) {
        //企业散客
        String customerCode = customer.getCustomerCode();
        final String costCenter1 = customer.getVipName();
        remark = "更改客户为：散客(" + customer.getVipName() + ")";

        retCode = em.createQuery("update BillInfo o set o.customer = :customer, o.customerCode = :customerCode, " +
                "o.customerType = :customerType, o.costCenter = :costCenter, o.projectName = :projectName " +
                "where o.enterpriseId = :enterpriseId and o.id = :billId and (o.audited is null or o.audited <> :auditStatus) ")
                .setParameter("enterpriseId", enterpriseId)
                .setParameter("billId", billId)
                .setParameter("auditStatus", "1")
                .setParameter("customer", null)
                .setParameter("customerType", customerType)
                .setParameter("customerCode", customerCode)
                .setParameter("costCenter", costCenter1)
                .setParameter("projectName", projectName)
                .executeUpdate();
      } else {
        // 企业
        customerType = 1;
        remark = "更改客户为：" + customer.getVipName();
        retCode = em.createQuery("update BillInfo o set o.customer = :customer, o.customerCode = :customerCode, " +
                "o.customerType = 1, o.costCenter = :costCenter, o.projectName = :projectName " +
                "where o.enterpriseId = :enterpriseId and o.id = :billId and (o.audited is null or o.audited <> :auditStatus) ")
                .setParameter("enterpriseId", enterpriseId)
                .setParameter("billId", billId)
                .setParameter("auditStatus", "1")
                .setParameter("customer", customer)
                .setParameter("customerCode", customer.getCustomerCode())
                .setParameter("costCenter", costCenter)
                .setParameter("projectName", projectName)
                .executeUpdate();

      }
    }

    if (retCode != 1) {
      throw new RuntimeException("To update bill's customer failed.");
    }

    BillInfo billInfo = em.find(BillInfo.class, billId);

    int count = 0;
    if (BILL_TYPE_FLIGHT.equalsIgnoreCase(billInfo.getBillType())) {
      //flight
      count = em.createQuery("update BillFlightTicket o set o.customer = :customer where o.enterpriseId = :enterpriseId and o.billNum = :billNum ")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("customer", customer)
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_FLIGHT_REFUND.equalsIgnoreCase(billInfo.getBillType())) {
      //refund
      count = em.createQuery("update BillFlightRefundTicket o set o.customerType = :customerType ,o.customer = :customer where o.enterpriseId = :enterpriseId and o.billNum = :billNum ")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("customer", customer)
              .setParameter("billNum", billInfo.getBillNum())
              .setParameter("customerType", customerType)
              .executeUpdate();
    } else if (BILL_TYPE_FLIGHT_CHANGE.equalsIgnoreCase(billInfo.getBillType())) {
      //flight change
      count = em.createQuery("update BillChangeOrder o set o.customerType = :customerType ,o.customer = :customer where o.enterpriseId = :enterpriseId and o.billNum = :billNum ")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("customer", customer)
              .setParameter("billNum", billInfo.getBillNum())
              .setParameter("customerType", customerType)
              .executeUpdate();
    } else if (BILL_TYPE_VAS.equalsIgnoreCase(billInfo.getBillType())) {
      //VAS
      count = em.createQuery("update BillVasOrder o set o.customerType = :customerType ,o.customer = :customer where o.enterpriseId = :enterpriseId and o.billNum = :billNum ")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("customer", customer)
              .setParameter("billNum", billInfo.getBillNum())
              .setParameter("customerType", customerType)
              .executeUpdate();
    } else if (BILL_TYPE_HOTEL.equalsIgnoreCase(billInfo.getBillType())) {
      //hotel
      count = em.createQuery("update BillHotelBooking o set o.customer = :customer where o.enterpriseId = :enterpriseId and o.billNum = :billNum ")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("customer", customer)
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_TRAIN.equalsIgnoreCase(billInfo.getBillType())) {
      // train
      count = em.createQuery("update BillTrainTicket o set o.customer = :customer where o.enterpriseId = :enterpriseId and o.billNum = :billNum ")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("customer", customer)
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    }

    if (count < 1) {
      throw new RuntimeException("to update bill's customer failed");
    }

    OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billInfo.getId(), remark);
    em.persist(log);


    return retCode;
  }

  /**
   * 保存机票账单
   *
   * @param billInfo      待保存的机票账单
   * @param flightOrderId 和机票账单对应的机票订单id
   */
  @Override
  public int createFlightBill(BillInfo billInfo, final int flightOrderId) {
    if (billInfo.getTickets().isEmpty()) {
      throw new RuntimeException("Tickets can't be empty.");
    }

    em.persist(billInfo);

    for (BillFlightTicket ticket : billInfo.getTickets()) {
      if (ticket.getDetails().isEmpty()) {
        throw new RuntimeException("Flights can't be empty.");
      }

      em.persist(ticket);

      for (BillFlightTicketDetail detail : ticket.getDetails()) {
        em.persist(detail);
      }
    }

    OperationLog log = OperationLogFactory.createBillOperationLog(billInfo.getEnterpriseId(), billInfo.getOperator(), billInfo.getId(), "创建机票账单");
    em.persist(log);

    int retCode = em.createQuery("update FlightOrder o set o.billImported = 1 where o.id = :id")
            .setParameter("id", flightOrderId)
            .executeUpdate();
    if (retCode != 1) {
      throw new RuntimeException("To update bill_imported failed.");
    }


    return billInfo.getId();
  }

  @Override
  public int createFlightRefundBill(BillInfo billInfo) {
    em.persist(billInfo);
    for (BillFlightRefundTicket refundTicket : billInfo.getRefundTickets()) {
      em.persist(refundTicket);
    }

    OperationLog log = OperationLogFactory.createBillOperationLog(billInfo.getEnterpriseId(), billInfo.getOperator(), billInfo.getId(), "创建退票账单");
    em.persist(log);


    return  billInfo.getId();
  }

  @Override
  public int createFlightChangeBill(BillInfo billInfo) {
    int retCode;

    em.persist(billInfo);
    for (BillChangeOrder o : billInfo.getChangeOrders()) {
      em.persist(o);
    }

    OperationLog log = OperationLogFactory.createBillOperationLog(billInfo.getEnterpriseId(), billInfo.getOperator(), billInfo.getId(), "创建改签账单");
    em.persist(log);


    retCode = billInfo.getId();

    return retCode;
  }

  @Override
  public int createVasBill(BillInfo billInfo) {
    int retCode;

    em.persist(billInfo);
    for (BillVasOrder vas : billInfo.getVasOrders()) {
      em.persist(vas);
    }


    OperationLog log = OperationLogFactory.createBillOperationLog(billInfo.getEnterpriseId(), billInfo.getOperator(),
            billInfo.getId(), "创建服务账单");
    em.persist(log);

    retCode = billInfo.getId();

    return retCode;
  }

  @Override
  public List<BillInfo> searchBillInfos(int enterpriseId, int flightOrderId) {
    return em.createQuery("select o from BillInfo o where o.enterpriseId = :enterpriseId and o.flightOrderId = :flightOrderId", BillInfo.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("flightOrderId", flightOrderId)
            .getResultList();
  }

  /**
   * 根据订单号查找对应的账单
   *
   */
  @Override
  public List<BillInfo> searchBillInfos(int enterpriseId, String orderNo, String billType) {
    return em.createQuery("select o from BillInfo o where o.enterpriseId = :enterpriseId " +
            "and o.billType = :billType and o.orderNo = :orderNo", BillInfo.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billType", billType)
            .setParameter("orderNo", orderNo)
            .getResultList();
  }

  /**
   * 删除机票账单：要删除t301,t202,t203的数据
   *
   */
  @Override
  public int deleteBillInfo(final int enterpriseId, final int billId) {
    int retCode = 0;

    BillInfo billInfo = em.find(BillInfo.class, billId);
    em.refresh(billInfo);

    if (billInfo == null || billInfo.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("The bill is not existed.");
    }

    if ("1".equalsIgnoreCase(billInfo.getAudited())) {
      throw new RuntimeException("The bill is audited");
    }

    final String billNum = billInfo.getBillNum();

    if (BILL_TYPE_FLIGHT.equals(billInfo.getBillType())) {
      //机票账单
      List<BillFlightTicket> tickets = em.createQuery("select o from BillFlightTicket o where o.enterpriseId = :enterpriseId and o.billNum = :billNum", BillFlightTicket.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billNum)
              .getResultList();

      for (BillFlightTicket ticket : tickets) {
        String ticketClass = ticket.getTicketClass();
        String ticketNo = ticket.getTicketNo().trim();

        if (ticketNo.length() == 13) {
          ticketNo = ticketNo.substring(3);
        }

        em.createQuery("delete from BillFlightTicketDetail o where " +
                "o.enterpriseId = :enterpriseId and o.ticketClass = :ticketClass and o.ticketNo = :ticketNo")
                .setParameter("enterpriseId", enterpriseId)
                .setParameter("ticketClass", ticketClass)
                .setParameter("ticketNo", ticketNo)
                .executeUpdate();

        em.remove(ticket);
      }
    } else if (BILL_TYPE_FLIGHT_REFUND.equals(billInfo.getBillType())) {
      //退票账单
      em.createQuery("delete from BillFlightRefundTicket o where o.enterpriseId = :enterpriseId and o.billNum = :billNum")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billNum)
              .executeUpdate();
    } else if (BILL_TYPE_FLIGHT_CHANGE.equals(billInfo.getBillType())) {
      //改签账单
      em.createQuery("delete from BillChangeOrder o where o.enterpriseId = :enterpriseId and o.billNum = :billNum")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billNum)
              .executeUpdate();
    } else if (BILL_TYPE_VAS.equals(billInfo.getBillType())) {
      //服务账单
      em.createQuery("delete from BillVasOrder o where o.enterpriseId = :enterpriseId and o.billNum = :billNum")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billNum)
              .executeUpdate();
    } else if (BILL_TYPE_TRAIN.equals(billInfo.getBillType())) {
      //火车票账单
      em.createQuery("delete from BillTrainTicket o where o.enterpriseId = :enterpriseId and o.billNum = :billNum")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billNum)
              .executeUpdate();
    } else if (BILL_TYPE_HOTEL.equals(billInfo.getBillType())) {
      //酒店
      em.createQuery("delete from BillHotelBooking o where o.enterpriseId = :enterpriseId and o.billNum = :billNum")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billNum)
              .executeUpdate();
    } else if (BILL_TYPE_CAR.equals(billInfo.getBillType())) {
      //租车
      em.createQuery("delete from BillRentalCarOrder o where o.enterpriseId = :enterpriseId and o.billNum = :billNum")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billNum)
              .executeUpdate();
    } else {
      throw new RuntimeException("not implemented yet.");
    }

    em.remove(billInfo);


    retCode = 1;

    return retCode;
  }

  @Override
  public List<BillInfo> searchMyBillInfo(final int enterpriseId, SearchBillInfoCriteria sc) {
    List<BillInfo> results = null;

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<BillInfo> cq = cb.createQuery(BillInfo.class);
    Root<BillInfo> root = cq.from(BillInfo.class);
    root.alias("o");

    var predicates0 = buildPredicates_searchMyBillInfo(enterpriseId, sc, cb, root);

    cq.where(predicates0.toArray(new Predicate[predicates0.size()]));
    List<Order> orderByes = new ArrayList<>();

    if (sc.getOrderBy() == 1) {
      orderByes.add(cb.asc(root.get("customerId")));
      orderByes.add(cb.asc(root.get("customerCode")));
      orderByes.add(cb.asc(root.get("costCenter")));
      orderByes.add(cb.asc(root.get("billType")));
      orderByes.add(cb.asc(root.get("opDate")));
      orderByes.add(cb.asc(root.get("id")));
    } else if (sc.getOrderBy() == 2) {
      orderByes.add(cb.asc(root.get("billType")));
      orderByes.add(cb.asc(root.get("id")));
    } else if (sc.getOrderBy() == 0) {
      orderByes.add(cb.asc(root.get("id")));
    } else if (sc.getOrderBy() == 3) {
      orderByes.add(cb.desc(root.get("id")));
    }


    cq.orderBy(orderByes);

    //计算根据条件查询得出的数据总数
    CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
    Root<BillInfo> countRoot = countQuery.from(BillInfo.class);
    countRoot.alias("o");

    var predicates1 = buildPredicates_searchMyBillInfo(enterpriseId, sc, cb, countRoot);


    countQuery.select(cb.count(countRoot)).where(predicates1.toArray(new Predicate[predicates1.size()]));
    Long count = em.createQuery(countQuery).getSingleResult();

    int maxResults = count.intValue();
    if (maxResults > 0) {
      Page page = sc.getPage();
      page.setRowCount(maxResults);

      results = em.createQuery(cq)
              .setFirstResult(page.getFirstPosition())
              .setMaxResults(page.getPageSize()).getResultList();
    }

    return results == null ? new ArrayList<>() : results;
  }

  private List<Predicate> buildPredicates_searchMyBillInfo(int enterpriseId, SearchBillInfoCriteria sc, CriteriaBuilder cb, Root<BillInfo> root) {
    List<Predicate> predicates = new ArrayList<>();

    Predicate where = cb.conjunction();
    predicates.add(where);

    where = cb.equal(root.get(BillInfo_.enterpriseId), enterpriseId);
    predicates.add(where);

    if (sc.getBillNum().length() > 0) {
      where = cb.equal(root.get(BillInfo_.billNum), sc.getBillNum());
      predicates.add(where);
    }

    if (!"-1".equalsIgnoreCase(sc.getBillType())) {
      //账单类型
      where = cb.equal(root.get(BillInfo_.billType), sc.getBillType());
      predicates.add(where);
    }

    if ("0".equalsIgnoreCase(sc.getTicketStatus())) {
      //配合TPR查找 正常出票（含改签）
      where = cb.equal(root.get(BillInfo_.billType), BILL_TYPE_FLIGHT);
      where = cb.or(where, cb.equal(root.get(BillInfo_.billType), BILL_TYPE_FLIGHT_CHANGE));
      predicates.add(where);
    } else if ("4".equalsIgnoreCase(sc.getTicketStatus())) {
      //配合TPR 退票
      where = cb.equal(root.get(BillInfo_.billType), BILL_TYPE_FLIGHT_REFUND);
      predicates.add(where);
    }

    List<Integer> customerIds = searchCustomerIdsLikeName(enterpriseId, sc.getLikeCustomerName());
    if (sc.getCustomerId() == 0) {
      //散客
      where = cb.equal(root.get(BillInfo_.customerType), 0);
      predicates.add(where);
    } else if (sc.getCustomerId() > 0) {
      where = cb.equal(root.get(BillInfo_.customer), sc.getCustomerId());
      predicates.add(where);
    } else if (!customerIds.isEmpty()) {
      // 查找指定的一批客户
      where = cb.in(root.get(BillInfo_.customer).get("id")).value(customerIds);
      predicates.add(where);
    }

    if (sc.getCostCenter().length() > 0) {
      where = cb.like(root.get(BillInfo_.costCenter), "%" + sc.getCostCenter() + "%");
      predicates.add(where);
    }

    if ("0".equalsIgnoreCase(sc.getBePaid())) {
      // 未支付
      where = cb.isNull(root.get(BillInfo_.bePaid));
      where = cb.or(where, cb.equal(root.get(BillInfo_.bePaid), "0"));
      predicates.add(where);
    } else if ("1".equalsIgnoreCase(sc.getBePaid())) {
      // 已支付
      where = cb.equal(root.get(BillInfo_.bePaid), "1");
      predicates.add(where);
    }

    if (sc.getPaymentStatus() == 0) {
      //未支出
      where = cb.isNull(root.get(BillInfo_.paymentStatus));
      where = cb.or(where, cb.equal(root.get(BillInfo_.paymentStatus), 0));
      predicates.add(where);
    } else if (sc.getPaymentStatus() == 1) {
      //已支出
      where = cb.equal(root.get(BillInfo_.paymentStatus), 1);
      predicates.add(where);
    }

    if (sc.getReviewed() == 0) {
      // 未审核的
      where = cb.notEqual(root.get(BillInfo_.audited), "1");
      where = cb.or(where, cb.isNull(root.get(BillInfo_.audited)));
      predicates.add(where);
    } else if (sc.getReviewed() == 1) {
      // 审核过的
      predicates.add(cb.equal(root.get(BillInfo_.audited), "1"));
    }

    if (sc.getSettled() == 0) {
      // 未结算的账单
      where = cb.isNull(root.get(BillInfo_.settlementNo));
      predicates.add(where);
    }

    if (!sc.getSupplierIds().isEmpty()) {
      //同时查询多个供应商
      where = cb.in(root.get(BillInfo_.supplier).get("id")).value(sc.getSupplierIds());
      predicates.add(where);
    } else if (sc.getSupplierId() > 0) {
      // 供应商
      predicates.add(cb.equal(root.get(BillInfo_.supplier), sc.getSupplierId()));
    }


    if (sc.getBeginDate() != null && sc.getEndDate() != null) {
      where = cb.greaterThanOrEqualTo(root.get(BillInfo_.opDate), sc.getBeginDate());
      where = cb.and(where, cb.lessThanOrEqualTo(root.get(BillInfo_.opDate), sc.getEndDate()));
      predicates.add(where);
    } else if (sc.getBeginDate() != null) {
      where = cb.greaterThanOrEqualTo(root.get(BillInfo_.opDate), sc.getBeginDate());
      predicates.add(where);
    } else if (sc.getEndDate() != null) {
      where = cb.lessThanOrEqualTo(root.get(BillInfo_.opDate), sc.getEndDate());
      predicates.add(where);
    }

    if (sc.getEtdzDateBegin() != null && sc.getEtdzDateEnd() != null) {
      where = cb.greaterThanOrEqualTo(root.get(BillInfo_.etdzDate), sc.getEtdzDateBegin());
      where = cb.and(where, cb.lessThanOrEqualTo(root.get(BillInfo_.etdzDate), sc.getEtdzDateEnd()));
      predicates.add(where);
    } else if (sc.getEtdzDateBegin() != null) {
      where = cb.greaterThanOrEqualTo(root.get(BillInfo_.etdzDate), sc.getEtdzDateBegin());
      predicates.add(where);
    } else if (sc.getEtdzDateEnd() != null) {
      where = cb.lessThanOrEqualTo(root.get(BillInfo_.etdzDate), sc.getEtdzDateEnd());
      predicates.add(where);
    }

    // 审核日期
    if (sc.getAuditDateBegin() != null && sc.getAuditDateEnd() != null) {
      where = cb.greaterThanOrEqualTo(root.get(BillInfo_.auditDate), sc.getAuditDateBegin());
      where = cb.and(where, cb.lessThanOrEqualTo(root.get(BillInfo_.auditDate), sc.getAuditDateEnd()));
      predicates.add(where);
    } else if (sc.getAuditDateBegin() != null) {
      where = cb.greaterThanOrEqualTo(root.get(BillInfo_.auditDate), sc.getAuditDateBegin());
      predicates.add(where);
    } else if (sc.getAuditDateEnd() != null) {
      where = cb.lessThanOrEqualTo(root.get(BillInfo_.auditDate), sc.getAuditDateEnd());
      predicates.add(where);
    }

    // 结算日期
    if (sc.getSettleDateBegin() != null && sc.getSettleDateEnd() != null) {
      where = cb.greaterThanOrEqualTo(root.get(BillInfo_.settleDate), sc.getSettleDateBegin());
      where = cb.and(where, cb.lessThanOrEqualTo(root.get(BillInfo_.settleDate), sc.getSettleDateEnd()));
      predicates.add(where);
    } else if (sc.getSettleDateBegin() != null) {
      where = cb.greaterThanOrEqualTo(root.get(BillInfo_.settleDate), sc.getSettleDateBegin());
      predicates.add(where);
    } else if (sc.getSettleDateEnd() != null) {
      where = cb.lessThanOrEqualTo(root.get(BillInfo_.settleDate), sc.getSettleDateEnd());
      predicates.add(where);
    }

    if (sc.getPayType() > 0) {
      predicates.add(cb.equal(root.get(BillInfo_.payTypeNote), sc.getPayType() + ""));
    }

    if (sc.getRemark().length() > 0) {
      where = cb.like(root.get(BillInfo_.remark), "%" + sc.getRemark() + "%");
      predicates.add(where);
    }

    if (sc.getOrderNo().length() > 0 || sc.getFlightOrderNo().length() > 0) {
      final String orderNo = sc.getOrderNo().length() > 0 ? sc.getOrderNo() : sc.getFlightOrderNo();
      Predicate p = cb.equal(root.get(BillInfo_.orderNo), orderNo);
      predicates.add(cb.or(p, cb.equal(root.get(BillInfo_.flightOrderNo), orderNo)));
    }

    if (!sc.getFlightOrderNos().isEmpty()) {
      CriteriaBuilder.In<String> p0 = cb.in(root.get(BillInfo_.orderNo));
      for (String billNo : sc.getFlightOrderNos()) {
        p0 = p0.value(billNo);
      }

      CriteriaBuilder.In<String> p1 = cb.in(root.get(BillInfo_.flightOrderNo));
      for (String billNo : sc.getFlightOrderNos()) {
        p1 = p1.value(billNo);
      }

      predicates.add(cb.or(p0, p1));
    }

    if (sc.getTicketer().length() > 0) {
      predicates.add(cb.equal(root.get(BillInfo_.op2), sc.getTicketer()));
    }

    if (sc.getIntlTicket() >= 0) {
      predicates.add(cb.equal(root.get(BillInfo_.intlTicket), sc.getIntlTicket()));
    }

    if (sc.getCostCenter().length() > 0) {
      where = cb.like(root.get(BillInfo_.costCenter), "%" + sc.getCostCenter() + "%");
      predicates.add(where);
    }

    if (sc.getSettlementNo().length() > 0) {
      where = cb.like(root.get(BillInfo_.settlementNo), "%" + sc.getSettlementNo() + "%");
      predicates.add(where);
    }
    return predicates;
  }

  @Override
  public List<BillFlightTicket> searchBillFlightTickets(int enterpriseId, String billNum) {
    return searchBillFlightTickets(em, enterpriseId, billNum);
  }

  @Override
  public List<BillFlightTicket> searchBillFlightTicketsBySettlementNo(int enterpriseId, String settlementNo, String psgName, String ticketNo) {
    List<BillFlightTicket> results = em.createQuery("select o from BillFlightTicket o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in (" +
            "select b.billNum from BillInfo b where b.enterpriseId = :enterpriseId and b.settlementNo = :settlementNo" +
            ")  order by o.id", BillFlightTicket.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("settlementNo", settlementNo)
            .getResultList();

    if (psgName.length() > 0) {
      results = results.stream().filter((BillFlightTicket t) -> t.getPassengerName().contains(psgName)).collect(Collectors.toList());
    }
    if (ticketNo.length() > 0) {
      results = results.stream().filter((BillFlightTicket t) -> t.getTicketNo().contains(ticketNo)).collect(Collectors.toList());
    }

    // 获取行程详情
    for (BillFlightTicket ticket : results) {
      List<BillFlightTicketDetail> details = searchBillFlightTicketDetails(em, enterpriseId, ticket);
      ticket.setDetails(details);
    }

    return results;
  }

  @Override
  public List<BillTrainTicket> searchBillTrainTickets(int enterpriseId, String billNum) {
    return em.createQuery("select o from BillTrainTicket o where o.enterpriseId = :enterpriseId and o.billNum = :billNum order by o.id", BillTrainTicket.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNum", billNum)
            .getResultList();
  }

  @Override
  public List<BillTrainTicket> searchBillTrainTickets(int enterpriseId, List<String> billNums) {
    return em.createQuery("select o from BillTrainTicket o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in :billNums order by o.billNum", BillTrainTicket.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNums", billNums)
            .getResultList();

  }

  @Override
  public List<BillHotelBooking> searchBillHotelBookings(int enterpriseId, String billNum) {
    return em.createQuery("select o from BillHotelBooking o where o.enterpriseId = :enterpriseId and o.billNum = :billNum order by o.id", BillHotelBooking.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNum", billNum)
            .getResultList();
  }

  @Override
  public List<BillHotelBooking> searchBillHotelBookings(int enterpriseId, List<String> billNums) {
    return em.createQuery("select o from BillHotelBooking o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in :billNums order by o.billNum", BillHotelBooking.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNums", billNums)
            .getResultList();

  }

  @Override
  public List<BillVasOrder> searchBillVasOrders(int enterpriseId, String billNum) {
    return doSearchBillVasOrders(enterpriseId, billNum);
  }

  @Override
  public List<BillVasOrder> searchBillVasOrders(int enterpriseId, List<String> billNums) {
    return em.createQuery("select o from BillVasOrder o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in :billNums order by o.billNum", BillVasOrder.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNums", billNums)
            .getResultList();

  }

  @Override
  public BillInfo getBillInfo(int enterpriseId, String billNum) {
    List<BillInfo> infos = em.createQuery("select o from BillInfo o where o.enterpriseId = :enterpriseId and o.billNum = :billNum", BillInfo.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNum", billNum)
            .getResultList();

    return infos.isEmpty() ? null : infos.get(0);
  }

  @Override
  public int createTrainTicket(int enterpriseId, String username, BillTrainTicket ticket, BillInfo billInfo) {
    em.persist(ticket);
    em.persist(billInfo);

    OperationLog log = OperationLogFactory.createBillOperationLog(billInfo.getEnterpriseId(), username,
            billInfo.getId(), "创建火车票账单");
    em.persist(log);

    return billInfo.getId();
  }

  @Override
  public List<BillTrainTicket> searchTrainTickets(int enterpriseId, SearchBillInfoCriteria sc) {
    List<BillTrainTicket> results = null;

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<BillTrainTicket> cq = cb.createQuery(BillTrainTicket.class);
    Root<BillTrainTicket> root = cq.from(BillTrainTicket.class);
    root.alias("o");

    var predicates0 = buildPredicates_searchTrainTickets(enterpriseId, sc, cb, root);


    cq.where(predicates0.toArray(new Predicate[predicates0.size()]));
    List<Order> orderByes = new ArrayList<>();
    orderByes.add(cb.desc(root.get("id")));

    cq.orderBy(orderByes);

    //计算根据条件查询得出的数据总数
    CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
    Root<BillTrainTicket> countRoot = countQuery.from(BillTrainTicket.class);
    countRoot.alias("o");

    var predicates1 = buildPredicates_searchTrainTickets(enterpriseId, sc, cb, countRoot);

    countQuery.select(cb.count(countRoot)).where(predicates1.toArray(new Predicate[predicates1.size()]));
    Long count = em.createQuery(countQuery).getSingleResult();

    int maxResults = count.intValue();
    if (maxResults > 0) {
      Page page = sc.getPage();
      page.setRowCount(maxResults);
      int i = (page.getPageNo() - 1) * page.getPageSize();

      if (i < 0) {
        i = 0;
      }

      results = em.createQuery(cq)
              .setFirstResult(i)
              .setMaxResults(page.getPageSize()).getResultList();

    } else {
      results = new ArrayList<>();
    }

    return results;
  }

  private static List<Predicate> buildPredicates_searchTrainTickets(int enterpriseId, SearchBillInfoCriteria sc, CriteriaBuilder cb, Root<BillTrainTicket> root) {
    List<Predicate> predicates = new ArrayList<>();

    Predicate predicate = cb.conjunction();
    predicates.add(predicate);

    predicate = cb.equal(root.get(BillTrainTicket_.enterpriseId), enterpriseId);
    predicates.add(predicate);


    if (sc.getBeginDate() != null && sc.getEndDate() != null) {
      predicate = cb.greaterThanOrEqualTo(root.get(BillTrainTicket_.saleDate), sc.getBeginDate());
      predicate = cb.and(predicate, cb.lessThanOrEqualTo(root.get(BillTrainTicket_.saleDate), sc.getEndDate()));
      predicates.add(predicate);
    } else if (sc.getBeginDate() != null) {
      predicate = cb.greaterThanOrEqualTo(root.get(BillTrainTicket_.saleDate), sc.getBeginDate());
      predicates.add(predicate);
    } else if (sc.getEndDate() != null) {
      predicate = cb.lessThanOrEqualTo(root.get(BillTrainTicket_.saleDate), sc.getEndDate());
      predicates.add(predicate);
    }

    if (sc.getCustomerId() == 0) {
      // 散客
      predicate = cb.isNull(root.get(BillTrainTicket_.customerId));
      predicates.add(predicate);
    } else if (sc.getCustomerId() > 0) {
      // 指定企业客户
      predicate = cb.equal(root.get(BillTrainTicket_.customerId), sc.getCustomerId());
      predicates.add(predicate);
    }

    if (sc.getName() != null && sc.getName().length() > 0) {
      // 乘客姓名
      predicate = cb.like(root.get(BillTrainTicket_.passengerName), "%" + sc.getName() + "%");
      predicates.add(predicate);
    }

    if (sc.getDepartureDate() != null && sc.getDepartureDate().length() == 10) {
      // 乘车日期
      try {
        predicate = cb.equal(root.get(BillTrainTicket_.ddate), DateUtil.convertToDate(sc.getDepartureDate(), "yyyy-MM-dd"));
        predicates.add(predicate);
      } catch (ParseException e) {
        logger.error(e.getMessage());
      }
    }
    return predicates;
  }

  /**
   * 保存酒店账单
   *
   */
  @Override
  public int createHotelBooking(int enterpriseId, String username, BillHotelBooking ticket, BillInfo billInfo) {
    em.persist(ticket);
    em.persist(billInfo);

    OperationLog log = OperationLogFactory.createBillOperationLog(billInfo.getEnterpriseId(), username,
            billInfo.getId(), "创建酒店账单");
    em.persist(log);

    return billInfo.getId();
  }

  /**
   * 查找酒店账单
   *
   */
  @Override
  public List<BillHotelBooking> searchHotelBookings(int enterpriseId, SearchBillInfoCriteria sc) {
    List<BillHotelBooking> results = null;

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<BillHotelBooking> cq = cb.createQuery(BillHotelBooking.class);
    Root<BillHotelBooking> root = cq.from(BillHotelBooking.class);
    root.alias("o");

    var predicates0 = buildPredicates_searchHotelBooking(enterpriseId, sc, cb, root);


    cq.where(predicates0.toArray(new Predicate[predicates0.size()]));
    List<Order> orderByes = new ArrayList<>();
    orderByes.add(cb.desc(root.get("id")));

    cq.orderBy(orderByes);

    //计算根据条件查询得出的数据总数
    CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
    Root<BillHotelBooking> countRoot = countQuery.from(BillHotelBooking.class);
    countRoot.alias("o");

    var predicates1 = buildPredicates_searchHotelBooking(enterpriseId, sc, cb, countRoot);

    countQuery.select(cb.count(countRoot)).where(predicates1.toArray(new Predicate[predicates1.size()]));
    Long count = em.createQuery(countQuery).getSingleResult();

    int maxResults = count.intValue();
    if (maxResults > 0) {
      Page page = sc.getPage();
      page.setRowCount(maxResults);
      int i = (page.getPageNo() - 1) * page.getPageSize();

      if (i < 0) {
        i = 0;
      }

      results = em.createQuery(cq)
              .setFirstResult(i)
              .setMaxResults(page.getPageSize()).getResultList();

    } else {
      results = new ArrayList<>();
    }

    return results;

  }

  private static List<Predicate> buildPredicates_searchHotelBooking(int enterpriseId, SearchBillInfoCriteria sc, CriteriaBuilder cb, Root<BillHotelBooking> root) {
    List<Predicate> predicates = new ArrayList<>();

    Predicate predicate = cb.conjunction();
    predicates.add(predicate);

    predicate = cb.equal(root.get(BillHotelBooking_.enterpriseId), enterpriseId);
    predicates.add(predicate);


    if (sc.getBeginDate() != null && sc.getEndDate() != null) {
      predicate = cb.greaterThanOrEqualTo(root.get(BillHotelBooking_.saleDate), sc.getBeginDate());
      predicate = cb.and(predicate, cb.lessThanOrEqualTo(root.get(BillHotelBooking_.saleDate), sc.getEndDate()));
      predicates.add(predicate);
    } else if (sc.getBeginDate() != null) {
      predicate = cb.greaterThanOrEqualTo(root.get(BillHotelBooking_.saleDate), sc.getBeginDate());
      predicates.add(predicate);
    } else if (sc.getEndDate() != null) {
      predicate = cb.lessThanOrEqualTo(root.get(BillHotelBooking_.saleDate), sc.getEndDate());
      predicates.add(predicate);
    }

    if (sc.getCustomerId() == 0) {
      // 散客
      predicate = cb.isNull(root.get(BillHotelBooking_.customerId));
      predicates.add(predicate);
    } else if (sc.getCustomerId() > 0) {
      // 指定企业客户
      predicate = cb.equal(root.get(BillHotelBooking_.customerId), sc.getCustomerId());
      predicates.add(predicate);
    }

    if (sc.getName() != null && sc.getName().length() > 0) {
      // 乘客姓名
      predicate = cb.like(root.get(BillHotelBooking_.passengerName), "%" + sc.getName() + "%");
      predicates.add(predicate);
    }

    if (sc.getHotelName() != null && sc.getHotelName().length() > 0) {
      // 酒店名称
      predicate = cb.like(root.get(BillHotelBooking_.hotelName), "%" + sc.getHotelName() + "%");
      predicates.add(predicate);
    }

    if (sc.getDepartureDate() != null && sc.getDepartureDate().length() == 10) {
      // 乘车日期
      try {
        predicate = cb.equal(root.get(BillHotelBooking_.ddate), DateUtil.convertToDate(sc.getDepartureDate(), "yyyy-MM-dd"));
        predicates.add(predicate);
      } catch (ParseException e) {
        logger.error(e.getMessage());
      }
    }
    return predicates;
  }

  /**
   * 查找未审核账单的机票
   *
   * @param enterpriseId 企业id
   * @param sc           查找条件
   * @param status       0-未审核的，1-已审核的
   */
  @Override
  public List<BillFlightTicket> searchBillFlightTickets(final int enterpriseId, SearchFlightOrderCriteria sc, int status) {
    List<BillFlightTicket> results = null;

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<BillFlightTicket> cq = cb.createQuery(BillFlightTicket.class);
    Root<BillFlightTicket> root = cq.from(BillFlightTicket.class);
    root.alias("o");

    var predicates0 = buildPredicates_searchBillFlightTickets(enterpriseId, sc, status, cb, root, cq);


    cq.where(predicates0.toArray(new Predicate[predicates0.size()]));
    List<Order> orderByes = new ArrayList<>();
    orderByes.add(cb.desc(root.get("id")));

    cq.orderBy(orderByes);

    //计算根据条件查询得出的数据总数
    CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
    Root<BillFlightTicket> countRoot = countQuery.from(BillFlightTicket.class);
    countRoot.alias("o");

    var predicates1 = buildPredicates_searchBillFlightTickets(enterpriseId, sc, status, cb, countRoot, cq);

    countQuery.select(cb.count(countRoot)).where(predicates1.toArray(new Predicate[predicates1.size()]));
    Long count = em.createQuery(countQuery).getSingleResult();

    int maxResults = count.intValue();
    if (maxResults > 0) {
      Page page = sc.getPage();
      page.setRowCount(maxResults);

      results = em.createQuery(cq)
              .setFirstResult(page.getFirstPosition())
              .setMaxResults(page.getPageSize()).getResultList();
    }
    if (results == null) {
      results = new ArrayList<>();
    }

    return results;
  }

  private static List<Predicate> buildPredicates_searchBillFlightTickets(int enterpriseId, SearchFlightOrderCriteria sc, int status, CriteriaBuilder cb, Root<BillFlightTicket> root, CriteriaQuery<BillFlightTicket> cq) {
    List<Predicate> predicates = new ArrayList<>();

    Predicate predicate = cb.conjunction();
    predicates.add(predicate);

    // 当前用户企业
    predicate = cb.equal(root.get(BillFlightTicket_.enterpriseId), enterpriseId);
    predicates.add(predicate);

    // 未销账的
    predicate = cb.isNull(root.get(BillFlightTicket_.bePaid));
    predicate = cb.or(predicate, cb.equal(root.get(BillFlightTicket_.bePaid), "0"));
    predicates.add(predicate);


    if (sc.getBeginDate() != null) {
      predicate = cb.greaterThanOrEqualTo(root.get(BillFlightTicket_.etdzDate), sc.getBeginDate());
      predicates.add(predicate);
    }

    if (sc.getEndDate() != null) {
      predicate = cb.lessThanOrEqualTo(root.get(BillFlightTicket_.etdzDate), sc.getEndDate());
      predicates.add(predicate);
    }

    if (sc.getName() != null && sc.getName().length() > 0) {
      predicate = cb.like(root.get(BillFlightTicket_.passengerName), "%" + sc.getName().trim() + "%");
      predicates.add(predicate);
    }

    if (sc.getTicketNo().length() > 0) {
      predicate = cb.like(root.get(BillFlightTicket_.ticketNo), "%" + sc.getTicketNo() + "%");
      predicates.add(predicate);
    }


    if (sc.getCustomerId() == 0) {
      // 散客
      predicate = cb.isNull(root.get(BillFlightTicket_.customerId));
      predicates.add(predicate);
    } else if (sc.getCustomerId() > 0) {
      // 指定企业客户
      predicate = cb.equal(root.get(BillFlightTicket_.customerId), sc.getCustomerId());
      predicates.add(predicate);
    }


    Subquery<String> subQuery = cq.subquery(String.class);
    Root<BillInfo> subRoot = subQuery.from(BillInfo.class);
    subQuery.select(subRoot.get(BillInfo_.billNum));
    subQuery.alias("b");

    List<Predicate> subQueryPredicates = new ArrayList<>();
    subQueryPredicates.add(cb.equal(subRoot.get(BillInfo_.enterpriseId), enterpriseId));

    if (status == 0) {
      Predicate predicate1 = cb.isNull(subRoot.get(BillInfo_.audited));
      predicate1 = cb.or(predicate1, cb.equal(subRoot.get(BillInfo_.audited), "0"));
      subQueryPredicates.add(predicate1);
    } else if (status == 1) {
      Predicate predicate1 = cb.equal(subRoot.get(BillInfo_.audited), "1");
      subQueryPredicates.add(predicate1);
    }

    subQuery.where(subQueryPredicates.toArray(new Predicate[]{}));


    predicates.add(root.get(BillFlightTicket_.billNum).in(subQuery));
    return predicates;
  }

  /**
   * 将订单设置为已审核状态
   */
  @Override
  public int auditBillInfoDone(int enterpriseId, String username, final int billId, String remark) {
    int retCode = em.createQuery("update BillInfo o " +
            "set o.audited = :audited, o.op3 = :op3, o.lastUpdate = :lastUpdate, o.auditDate = :auditDate " +
            "where o.enterpriseId = :enterpriseId and o.id = :id and (o.audited is null or o.audited = :notAudited)")
            .setParameter("id", billId)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("notAudited", BILL_NOT_AUDITED)
            .setParameter("op3", username)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .setParameter("auditDate", DateUtil.getCurDate())
            .setParameter("audited", BILL_AUDITED)
            .executeUpdate();
    if (retCode != 1) {
      throw new RuntimeException("It failed to audit bill.");
    }


    OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, remark);
    em.persist(log);

    return retCode;
  }

  /**
   * 取消账单已审核状态
   */
  @Override
  public int cancelBillInfoAuditPassed(final int enterpriseId, final String username, final int id, final String remark) {
    int retCode = 0;

    BillInfo billInfo = em.find(BillInfo.class, id);
    em.refresh(billInfo);

    if (billInfo.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("Enterprise id is not equal.");
    }

    if (billInfo.getAudited().equalsIgnoreCase(BILL_NOT_AUDITED)) {
      throw new RuntimeException("Bill is not audited.");
    }

    if (!StringUtils.isEmpty(billInfo.getSettlementNo())) {
      throw new RuntimeException("This bill is in a settlement sheet(" + billInfo.getSettlementNo() + ")");
    }

    if (CUSTOMER_PAYMENT_RECEIVED.equalsIgnoreCase(billInfo.getBePaid())) {
      throw new RuntimeException("This bill is paid.");
    }

    billInfo.setAudited(BILL_NOT_AUDITED);
    billInfo.setAuditDate(null);
    billInfo.setLastUpdate(DateUtil.getCurDateTime());

    //判断是否已将此金额加入到的企业现金账户中，如果已加入，则需要从其中扣除
    if (billInfo.getBeProcessed() == 1) {
      billInfo.setBeProcessed(0);

      // 已经处理进入企业现金账户
      final int customerEntId = billInfo.getCustomer().getCustomerEnterpriseId();
      EnterpriseBalance bal = doGetEnterpriseBalance(enterpriseId, customerEntId, em);
      final int billSum = (int) billInfo.getBillSum() * 100;
      retCode = em.createQuery("update EnterpriseBalance o set o.cashBalance = o.cashBalance + :billsum, o.lastUpdate = :lastUpdate " +
              " where o.enterpriseId = :authorizee and o.authorizer = :authorizer")
              .setParameter("authorizee", customerEntId)
              .setParameter("authorizer", enterpriseId)
              .setParameter("billsum", billSum) // 转化为分
              .setParameter("lastUpdate", DateUtil.getCurDateTime())
              .executeUpdate();
      if (retCode != 1) {
        throw new RuntimeException("To update EnterpriseBalance failed.");
      }

      em.flush();
      em.refresh(bal);

      //企业客户的财务记录日志
      AmountHistory amountHistory = new AmountHistory();
      amountHistory.setEnterpriseId(customerEntId);
      amountHistory.setCredit(billSum);
      amountHistory.setOrderType(30);
      amountHistory.setOrderId(id);
      amountHistory.setRemark("账单(" + billInfo.getBillNum() + ")取消审核，原因：" + remark);
      amountHistory.setAccountBalance(bal.getCashBalance());

      em.persist(amountHistory);
    }

    em.merge(billInfo);

    OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, id, "取消审核，原因: " + remark);
    em.persist(log);

    return 1;
  }

  @Override
  public List<BillFlightRefundTicket> searchBillFlightRefundTickets(int enterpriseId, String billNum) {
    return searchBillFlightRefundTickets(em, enterpriseId, billNum);
  }

  @Override
  public List<BillFlightRefundTicket> searchBillFlightRefundTickets(int enterpriseId, List<String> billNums) {
    return em.createQuery("select o from BillFlightRefundTicket o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in :billNums order by o.billNum, o.ticketNo", BillFlightRefundTicket.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNums", billNums)
            .getResultList();
  }

  @Override
  public List<BillChangeOrder> searchBillChangeOrders(int enterpriseId, String billNum) {
    return em.createQuery("select o from BillChangeOrder o " +
            "where o.enterpriseId = :enterpriseId and o.billNum = :billNum order by o.ticketNo", BillChangeOrder.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNum", billNum)
            .getResultList();
  }

  @Override
  public List<BillChangeOrder> searchBillChangeOrders(int enterpriseId, List<String> billNums) {
    return em.createQuery("select o from BillChangeOrder o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in :billNums order by o.billNum, o.ticketNo", BillChangeOrder.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNums", billNums)
            .getResultList();
  }

  @Override
  public int batchReviewBillInfos(int enterpriseId, final String username, int[] billIds, final double total) {
    int retCode = 0;

    List<Integer> ids = new ArrayList<>();
    for (int billId : billIds) {
      ids.add(billId);
    }

    List<BillInfo> infos = em.createQuery("select o from BillInfo o where o.enterpriseId = :enterpriseId and o.id in :ids", BillInfo.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("ids", ids)
            .getResultList();
    boolean b = true;
    double total1 = 0;
    for (BillInfo info : infos) {
      total1 += info.getBillSum();
      if (BILL_AUDITED.equalsIgnoreCase(info.getAudited())) {
        throw new RuntimeException("账单(" + info.getBillNum() + ")已审核.");
      }
    }

    //校验总数是否相等
    double alpha = Math.abs(total1 - total);
    if (alpha > 0.0001) {
      b = false;
    }

    if (b) {
      for (BillInfo info : infos) {
        info.setOp3(username);
        info.setAudited(BILL_AUDITED);
        info.setAuditDate(DateUtil.getCurDate());
        em.merge(info);

        OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, info.getId(), "(" + ids.size() + ")批量审核通过。");
        em.persist(log);
      }


      retCode = 1;
    } else {
      throw new RuntimeException("rollback");
    }

    return retCode;
  }

  /**
   * 自动审核账单
   * 核对票号，出票日期需要相符，如果出票日期不符可以在账单的备注里提示
   * 乘机人姓名暂时不匹配
   * 应付金额需要相等
   * 自动审核完成也在账单备注里注释
   *
   */
  @Override
  public int autoReviewBillInfo(final int enterpriseId, final String username, final int billId, final int ticketStatus) {
    int retCode = 0;

    BillInfo billInfo = em.find(BillInfo.class, billId);
    if (billInfo == null || billInfo.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("Bill is not existed.");
    }

    em.refresh(billInfo);

    if (billInfo.getAudited().equalsIgnoreCase(BILL_AUDITED)) {
      throw new RuntimeException("Bill has been audited.");
    }

    double totalCost0 = 0;
    double totalCost1 = 0;
    boolean b = true;
    boolean etdzDateEqual = true; // 出票日期是否相同，默认相同，如果不同则不能自动审核，做账单备注提示

    //TicketSale列表，用于保存哪些票号匹配成功
    List<TicketSale> ticketSales = new ArrayList<>();

    if (ticketStatus == TICKET_STATUS_PRINTED) {
      // 该账单包含的机票
      List<BillFlightTicket> tickets = searchBillFlightTickets(em, enterpriseId, billInfo.getBillNum());

      for (BillFlightTicket ticket : tickets) {
        totalCost0 += ticket.getCost();
        //查找票号，如果没有找到，则 b = false;
        final String ticketNo = ticket.getBalanceCode() + "-" + ticket.getTicketNo();

        List<TicketSale> results = this.searchTicketSale(enterpriseId, ticketNo, ticketStatus);
        if (results.size() == 1) {
          TicketSale ts = results.get(0);
          ticketSales.add(ts);

          totalCost1 += ts.getBottomPrice();
          if (ts.getSellDate().getTime() != ticket.getEtdzDate().getTime()) {
            etdzDateEqual = false;
          }
        } else {
          b = false;
          break;
        }
      }
    } else if (ticketStatus == TICKET_STATUS_EX) {
      List<BillChangeOrder> changeOrders = this.searchBillChangeOrders(enterpriseId, billInfo.getBillNum());

      for (BillChangeOrder co : changeOrders) {
        totalCost0 += co.getAirChangeCharge();
        //查找票号，如果没有找到，则 b = false;
        final String ticketNo = co.getBalanceCode() + "-" + co.getTicketNo();
        List<TicketSale> results = this.searchTicketSale(enterpriseId, ticketNo, ticketStatus);
        if (results.isEmpty() && co.getNewTicketNo().length() == 14) {
          results = this.searchTicketSale(enterpriseId, co.getNewTicketNo(), ticketStatus);
        }
        if (results.size() == 1) {
          TicketSale ts = results.get(0);
          ticketSales.add(ts);

          totalCost1 += ts.getTax() + ts.getPrice();
          if (ts.getSellDate().getTime() != co.getOrderDate().getTime()) {
            etdzDateEqual = false;
          }
        } else {
          b = false;
          break;
        }
      }
    } else if (ticketStatus == TICKET_STATUS_REFUND) {
      List<BillFlightRefundTicket> tickets = this.searchBillFlightRefundTickets(enterpriseId, billInfo.getBillNum());

      for (BillFlightRefundTicket t : tickets) {
        totalCost0 += t.getAirlineRefundAmount();
        //查找票号，如果没有找到，则 b = false;
        final String ticketNo = t.getBalanceCode() + "-" + t.getTicketNo();
        List<TicketSale> results = this.searchTicketSale(enterpriseId, ticketNo, ticketStatus);
        if (results.size() == 1) {
          TicketSale ts = results.get(0);
          ticketSales.add(ts);

          totalCost1 += ts.getPrice();
          if (ts.getSellDate().getTime() != t.getOrderDate().getTime()) {
            etdzDateEqual = false;
          }
        } else {
          b = false;
          break;
        }
      }
    } else {
      throw new RuntimeException("not implemented");
    }


    if (!b) {
      throw new RuntimeException("rollback 0");
    }

    final double alpha = Math.abs(totalCost0 - totalCost1);
    if (alpha > 0.00000001) {
      // 金额不匹配
      logger.info("Bill(" + billInfo.getBillNum() + ") amount is not equal(" + totalCost0 + " <> " + totalCost1 + ").");
      b = false;
    }

    if (!b) {
      throw new RuntimeException("rollback 1");
    } else if (b && !etdzDateEqual) {
      //金额匹配，出票日期不匹配
      OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "自审：日期不符.");
      em.persist(log);

      retCode = em.createQuery("update BillInfo o set o.remark = :remark where o.id = :id")
              .setParameter("id", billInfo.getId())
              .setParameter("remark", StringUtil.mergedRemark(billInfo.getRemark(), "自审：日期不符"))
              .executeUpdate();
      if (retCode != 1) {
        throw new RuntimeException("rollback");
      }

      logger.info("Bill(" + billInfo.getBillNum() + ") can be auto audited, but etdz is not equal.");
    } else {
      // 自动审核可以通过
      for (TicketSale ts : ticketSales) {
        ts.setCheckStatus(2);
        ts.setBillNum(billInfo.getBillNum());
        ts.setLastupdate(DateUtil.getCurDateTime());

        em.merge(ts);
      }

      //金额匹配，出票日期匹配
      OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "自审通过, 金额：" + totalCost1 + ".");
      em.persist(log);

      retCode = em.createQuery("update BillInfo o set o.remark = :remark where o.id = :id")
              .setParameter("id", billInfo.getId())
              .setParameter("remark", StringUtil.mergedRemark(billInfo.getRemark(), "自审通过, 金额：" + totalCost1))
              .executeUpdate();
      if (retCode != 1) {
        logger.info("Bill(" + billInfo.getBillNum() + ") can be audited, but failed.");
        throw new RuntimeException("rollback");
      }
    }

    return retCode;
  }


  @Override
  public List<BillInfo> searchBillInfoBySettlementNo(int enterpriseId, String settlementNo, String billNum,
                                                     String orderNo, Date billDate, String flightOrderNo, String psgName) {
    List<String> billNums = new ArrayList<>();
    if (StringUtils.isNotBlank(psgName)) {
      //查找乘机人姓名
      //机票
      List<String> list = em.createQuery("select distinct o.billNum from BillFlightTicket o where o.enterpriseId = :enterpriseId" +
              " and o.passengerName like :psgName", String.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("psgName", "%" + psgName + "%")
              .getResultList();
      billNums.addAll(list);
      //退票
      list = em.createQuery("select distinct o.billNum from BillFlightRefundTicket o where o.enterpriseId = :enterpriseId" +
              " and o.psgName like :psgName", String.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("psgName", "%" + psgName + "%")
              .getResultList();
      billNums.addAll(list);
      //改签
      list = em.createQuery("select distinct o.billNum from BillChangeOrder o where o.enterpriseId = :enterpriseId" +
              " and o.psgName like :psgName", String.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("psgName", "%" + psgName + "%")
              .getResultList();
      billNums.addAll(list);
      //保险 BillVasOrder
      list = em.createQuery("select distinct o.billNum from BillVasOrder o where o.enterpriseId = :enterpriseId" +
              " and o.passengerName like :psgName", String.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("psgName", "%" + psgName + "%")
              .getResultList();
      billNums.addAll(list);
      //酒店 BillVasOrder
      list = em.createQuery("select distinct o.billNum from BillHotelBooking o where o.enterpriseId = :enterpriseId" +
              " and o.passengerName like :psgName", String.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("psgName", "%" + psgName + "%")
              .getResultList();
      billNums.addAll(list);
      //火车票 BillVasOrder
      list = em.createQuery("select distinct o.billNum from BillTrainTicket o where o.enterpriseId = :enterpriseId" +
              " and o.passengerName like :psgName", String.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("psgName", "%" + psgName + "%")
              .getResultList();
      billNums.addAll(list);
      //用车
      list = em.createQuery("select distinct o.billNum from BillRentalCarOrder o where o.enterpriseId = :enterpriseId" +
              " and o.linkman like :psgName", String.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("psgName", "%" + psgName + "%")
              .getResultList();
      billNums.addAll(list);
    }

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<BillInfo> cq = cb.createQuery(BillInfo.class);
    Root<BillInfo> root = cq.from(BillInfo.class);
    root.alias("o");

    List<Predicate> predicates = new ArrayList<>();

    Predicate where = cb.conjunction();
    predicates.add(where);

    where = cb.equal(root.get(BillInfo_.enterpriseId), enterpriseId);
    predicates.add(where);

    where = cb.equal(root.get(BillInfo_.settlementNo), settlementNo);
    predicates.add(where);


    if (billNum != null && billNum.trim().length() > 0) {
      where = cb.equal(root.get(BillInfo_.billNum), billNum);
      predicates.add(where);
    }

    if (orderNo != null && orderNo.trim().length() > 0) {
      where = cb.equal(root.get(BillInfo_.orderNo), orderNo);
      predicates.add(where);
    }

    if (StringUtils.isNotBlank(flightOrderNo)) {
      where = cb.equal(root.get(BillInfo_.flightOrderNo), flightOrderNo);
      predicates.add(where);
    }

    if (billDate != null) {
      where = cb.equal(root.get(BillInfo_.opDate), billDate);
      predicates.add(where);
    }

    if (!billNums.isEmpty()) {
      CriteriaBuilder.In<String> predicate0 = cb.in(root.get(BillInfo_.billNum));

      for (String billNo : billNums) {
        predicate0 = predicate0.value(billNo);
      }

      predicates.add(predicate0);
    }


    //计算根据条件查询得出的数据总数
    cq.where(predicates.toArray(new Predicate[predicates.size()]));
    List<Order> orderByes = new ArrayList<>();
    orderByes.add(cb.asc(root.get("billType")));
    orderByes.add(cb.asc(root.get("id")));
    cq.orderBy(orderByes);

    return em.createQuery(cq).getResultList();
  }

  /**
   * 账单销账
   *
   * @param receiptId         收款单id
   * @param billId            账单id
   * @return 代码
   */
  @Override
  public int writeOffBill(final int receiptId, final int billId, final String operator, boolean ignoreNegativeNum) {
    //收款单的销账金额加上账单的金额，账单设置为已销账，账单对应的机票、火车票、酒店订单设置为已销账。
    // 记录到ReceiptHistory
    int retCode = 0;

    ReceiptInfo receiptInfo = em.find(ReceiptInfo.class, receiptId);
    em.refresh(receiptInfo);

    if (receiptInfo == null) {
      throw new RuntimeException("Receipt is not existed.");
    }

    BillInfo billInfo = em.find(BillInfo.class, billId);
    em.refresh(billInfo);
    if (billInfo == null) {
      throw new RuntimeException("Bill is not existed.");
    }

    if (billInfo.getEnterpriseId() != receiptInfo.getEnterpriseId()) {
      throw new RuntimeException("Bill and receipt do not belong to the same company.");
    }

    if (CUSTOMER_PAYMENT_RECEIVED.equals(billInfo.getBePaid())) {
      throw new RuntimeException("Bill is paid");
    }


    double totalAmount = receiptInfo.getWriteOffAmount() + billInfo.getBillSum();
    if (receiptInfo.getReceiptType() == 0 && !ignoreNegativeNum) {
      // 收款单
      if (receiptInfo.getAmount() < totalAmount) {
        throw new RuntimeException("Amount is less than write off amount.");
      }
    } else if (receiptInfo.getReceiptType() == 1) {
      // 退款单
      if (receiptInfo.getAmount() > totalAmount) {
        throw new RuntimeException("Amount(-) is greater than write-off-amount.");
      }
    }

    receiptInfo.setWriteOffAmount(totalAmount); //已销账金额
    receiptInfo.setLastUpdate(DateUtil.getCurDateTime());
    if (Double.compare(receiptInfo.getAmount(), receiptInfo.getWriteOffAmount()) == 0) {
      receiptInfo.setStatus(1);
    }

    billInfo.setBePaid(CUSTOMER_PAYMENT_RECEIVED);
    billInfo.setSettleDate(DateUtil.getCurDate());
    billInfo.setSettlementNo(MANUAL_WRITE_OFF_REMARK);
    billInfo.setWriteOffDate(DateUtil.getCurDate());
    billInfo.setReceiptNo(receiptInfo.getReceiptNo());
    billInfo.setWriteOffMemo(MANUAL_WRITE_OFF_REMARK);

    em.merge(billInfo);


    if (BILL_TYPE_FLIGHT.equalsIgnoreCase(billInfo.getBillType())) {
      retCode = em.createQuery("update BillFlightTicket o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_FLIGHT_REFUND.equalsIgnoreCase(billInfo.getBillType())) {
      // 退票
      retCode = em.createQuery("update BillFlightRefundTicket o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_FLIGHT_CHANGE.equalsIgnoreCase(billInfo.getBillType())) {
      // 改签
      retCode = em.createQuery("update BillChangeOrder o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_VAS.equalsIgnoreCase(billInfo.getBillType())) {
      // 服务单
      retCode = em.createQuery("update BillVasOrder o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_HOTEL.equalsIgnoreCase(billInfo.getBillType())) {
      retCode = em.createQuery("update BillHotelBooking o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_TRAIN.equalsIgnoreCase(billInfo.getBillType())) {
      retCode = em.createQuery("update BillTrainTicket o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    }

    em.merge(receiptInfo);
    // 收款单日志
    OperationLog log = OperationLogFactory.createReceiptOperationLog();
    log.setEnterpriseId(billInfo.getEnterpriseId());
    log.setOperator(operator);
    log.setOrderId(receiptInfo.getId());
    log.setContent("账单(" + billInfo.getBillNum() + ")销账，销账金额：" + billInfo.getBillSum() + ", 收款单余额：" + receiptInfo.getBalance());
    em.persist(log);

    ReceiptHistory rh = new ReceiptHistory();
    rh.setEnterpriseId(billInfo.getEnterpriseId());
    rh.setOperator(operator);
    rh.setReceiptId(receiptInfo.getId());
    rh.setReceiptNo(receiptInfo.getReceiptNo());
    rh.setDebt(billInfo.getBillSum());
    rh.setBalance(receiptInfo.getBalance());
    rh.setRemark("账单(" + billInfo.getBillNum() + ")销账.");
    em.persist(rh);

    // 账单日志
    var log1 = OperationLogFactory.createBillOperationLog(billInfo.getEnterpriseId(), operator, billId, "用收款单(" + receiptInfo.getReceiptNo() + ")销账");
    em.persist(log1);

    //此处需要强制保存到数据库。
    //因为此方法会被调用多次并且合并到一个事务中，如果保存到数据库，则下次调用时会导致数据错误。
    em.flush();

    return retCode;
  }

  @Override
  public int writeOffBill(final int enterpriseId, final int userId, final int billId) {
    if (enterpriseId != userId) {
      throw new RuntimeException("Not authenticated.");
    }

    int retCode = 0;

    BillInfo billInfo = em.find(BillInfo.class, billId);
    em.refresh(billInfo);
    if (billInfo == null) {
      throw new RuntimeException("Bill is not existed.");
    }

    if (billInfo.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("Bill and receipt do not belong to the same company.");
    }

    if (CUSTOMER_PAYMENT_RECEIVED.equals(billInfo.getBePaid())) {
      throw new RuntimeException("Bill is paid");
    }

    if (billInfo.getSettlementNo().length() > 0) {
      throw new RuntimeException("included in settlement(" + billInfo.getSettlementNo() + ")");
    }


    billInfo.setBePaid(CUSTOMER_PAYMENT_RECEIVED);
    billInfo.setSettleDate(DateUtil.getCurDate());
    billInfo.setSettlementNo(MANUAL_WRITE_OFF_REMARK);
    billInfo.setWriteOffDate(DateUtil.getCurDate());
    billInfo.setWriteOffMemo("超级管理员销账");

    em.merge(billInfo);


    if (BILL_TYPE_FLIGHT.equalsIgnoreCase(billInfo.getBillType())) {
      retCode = em.createQuery("update BillFlightTicket o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_FLIGHT_REFUND.equalsIgnoreCase(billInfo.getBillType())) {
      // 退票
      retCode = em.createQuery("update BillFlightRefundTicket o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_FLIGHT_CHANGE.equalsIgnoreCase(billInfo.getBillType())) {
      // 改签
      retCode = em.createQuery("update BillChangeOrder o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_VAS.equalsIgnoreCase(billInfo.getBillType())) {
      // 服务单
      retCode = em.createQuery("update BillVasOrder o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_HOTEL.equalsIgnoreCase(billInfo.getBillType())) {
      retCode = em.createQuery("update BillHotelBooking o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    } else if (BILL_TYPE_TRAIN.equalsIgnoreCase(billInfo.getBillType())) {
      retCode = em.createQuery("update BillTrainTicket o set o.bePaid = '1' where o.billNum = :billNum and o.enterpriseId = :enterpriseId")
              .setParameter("enterpriseId", billInfo.getEnterpriseId())
              .setParameter("billNum", billInfo.getBillNum())
              .executeUpdate();
    }

    User user = em.find(User.class, userId);

    // 账单日志
    OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, user.getUsername(), billId, "手工销账");
    em.persist(log);


    return retCode;
  }


  /**
   * 查找已审核的，未处理过的账单
   *
   */
  @Override
  public List<BillInfo> searchBillInfoNotProcessed(int max) {
    return em.createQuery("select o from BillInfo o " +
            "where (o.customerType is null or o.customerType = 1) " +
            "and (o.beProcessed is null or o.beProcessed = 0) " +
            "and o.audited = '1' and o.createTime >= :beginDate order by o.id", BillInfo.class)
            .setParameter("beginDate", DateUtil.addDays(DateUtil.getCurDate(), -365))
            .setMaxResults(max)
            .getResultList();
  }

  /**
   * 同步账单中的金额到企业客户的现金账户
   */
  @Override
  public void syncBillInfo2Balance(final int enterpriseId, final int customerEntId, final int id,
                                   final String billNum, final int billSum) {
    EnterpriseBalance bal = doGetEnterpriseBalance(enterpriseId, customerEntId, em);
    if (bal == null) {
      return;
    }

    int retCode = em.createQuery("update BillInfo o set o.beProcessed = 1 " +
            " where (o.beProcessed is null or o.beProcessed = 0) and o.audited = '1' and o.id = :id")
            .setParameter("id", id)
            .executeUpdate();
    if (retCode != 1) {
      throw new RuntimeException("update billInfo failed.");
    }

    retCode = em.createQuery("update EnterpriseBalance o set o.cashBalance = o.cashBalance - :billsum, " +
            "o.lastUpdate = :lastUpdate " +
            " where o.enterpriseId = :authorizee and o.authorizer = :authorizer")
            .setParameter("authorizee", customerEntId)
            .setParameter("authorizer", enterpriseId)
            .setParameter("billsum", billSum) // 转化为分
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .executeUpdate();
    if (retCode != 1) {
      throw new RuntimeException("To update EnterpriseBalance failed.");
    }

    em.flush();
    em.refresh(bal);

    //企业客户的财务记录日志
    AmountHistory amountHistory = new AmountHistory();
    amountHistory.setOwnerId(enterpriseId);
    amountHistory.setEnterpriseId(customerEntId);
    amountHistory.setDebt(billSum);
    amountHistory.setOrderType(30);
    amountHistory.setOrderId(id);
    amountHistory.setRemark("账单号：" + billNum);
    amountHistory.setAccountBalance(bal.getCashBalance());

    em.persist(amountHistory);

  }

  @Override
  public List<OperationLog> searchBillInfoHistory(int enterpriseId, int id) {
    return em.createQuery("select o from OperationLog o " +
            "where o.enterpriseId = :enterpriseId and o.orderId = :orderId and o.objectType = :ot order by o.id desc", OperationLog.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("orderId", id)
            .setParameter("ot", OPERATING_OBJECT_BILL)
            .getResultList();
  }

  /**
   * 修改账单的供应商
   */
  @Override
  public int updateBillSupplier(int enterpriseId, String username, int billId, Supplier supplier) {
    int retCode = em.createQuery("update BillInfo o set o.supplier = :supplier, o.lastUpdate = :lastUpdate " +
            "where o.id = :id and o.enterpriseId = :enterpriseId and (o.audited is null or o.audited = :audited) ")
            .setParameter("id", billId)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("supplier", supplier)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .setParameter("audited", BILL_NOT_AUDITED)
            .executeUpdate();

    if (retCode == 1) {
      OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "供应商更改为：" + supplier.getName());
      em.persist(log);
    }

    return retCode;
  }

  @Override
  public int updateBillInfoPayment(int enterpriseId, String username, int billId, PaymentMethod pm) {
    int retCode =  em.createQuery("update BillInfo o set o.paymentMethod = :pm, o.lastUpdate = :lastUpdate " +
            "where o.id = :id and o.enterpriseId = :enterpriseId and o.audited <> :audited")
            .setParameter("id", billId)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("pm", pm)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .setParameter("audited", "1")
            .executeUpdate();

    if (retCode == 1) {
      OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "支付更改为：" + pm.getName());
      em.persist(log);
    }


    return retCode;
  }

  /**
   * 修改机票账单的出票日期
   */
  @Override
  public int updateBillInfoEtdzDate(int enterpriseId, String username, final int billId, Date etdzDate) {
    int retCode = em.createQuery("update BillInfo o set o.etdzDate = :etdzDate, o.lastUpdate = :lastUpdate " +
            "where o.id = :id and o.enterpriseId = :enterpriseId and (o.audited is null or o.audited = '0')  and o.billType = :billType")
            .setParameter("id", billId)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billType", BILL_TYPE_FLIGHT)
            .setParameter("etdzDate", etdzDate)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .executeUpdate();

    if (retCode == 1) {
      // 更改对应的机票的出票日期
      BillInfo billInfo = em.find(BillInfo.class, billId);
      retCode = em.createQuery("update BillFlightTicket o set o.etdzDate = :etdzDate, o.lastUpdate = :lastUpdate " +
              "where o.enterpriseId = :enterpriseId and o.billNum = :billNum")
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billInfo.getBillNum())
              .setParameter("etdzDate", etdzDate)
              .setParameter("lastUpdate", DateUtil.getCurDateTime())
              .executeUpdate();
      if (retCode < 1) {
        throw new RuntimeException("update bill ticket etdz date failed.");
      }

      OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "出票日期更改为：" + DateUtil.formatDate(etdzDate, "yyyy-MM-dd"));
      em.persist(log);
    }


    return retCode;
  }

  /**
   * 修改账单日期
   *
   */
  @Override
  public int updateBillInfoOpDate(int enterpriseId, String username, int billId, Date opDate) {
    int retCode = em.createQuery("update BillInfo o set o.opDate = :opDate, o.lastUpdate = :lastUpdate " +
            "where o.id = :id and o.enterpriseId = :enterpriseId and (o.audited is null or o.audited = '0')")
            .setParameter("id", billId)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("opDate", opDate)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .executeUpdate();

    if (retCode == 1) {
      OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "账单日期更改为：" + DateUtil.formatDate(opDate, "yyyy-MM-dd"));
      em.persist(log);

    } else {
      throw new RuntimeException("rollback");
    }

    return retCode;
  }

  @Override
  public int updateBillInfoOp1(int enterpriseId, String username, int billId, String op1) {
    int retCode = em.createQuery("update BillInfo o set o.op1 = :op1, o.lastUpdate = :lastUpdate " +
            "where o.id = :id and o.enterpriseId = :enterpriseId and (o.audited is null or o.audited = :audited)")
            .setParameter("id", billId)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("op1", op1)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .setParameter("audited", "0")
            .executeUpdate();

    if (retCode == 1) {
      OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "预订人更改为：" + op1);
      em.persist(log);
    }

    return retCode;
  }

  @Override
  public int updateBillInfoRemark(int enterpriseId, String username, int billId, String remark) {
    int retCode = em.createQuery("update BillInfo o set o.remark = :remark, o.lastUpdate = :lastUpdate " +
            "where o.id = :id and o.enterpriseId = :enterpriseId")
            .setParameter("id", billId)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("remark", remark)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .executeUpdate();

    if (retCode == 1) {
      OperationLog log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "备注：" + remark);
      em.persist(log);
    }


    return retCode;
  }

  @Override
  public int updateBillInfoPayType(int enterpriseId, String username, int billId, String payType) {
    int retCode = em.createQuery("update BillInfo o set o.payTypeNote = :payType, o.lastUpdate = :lastUpdate " +
            "where o.id = :id and o.enterpriseId = :enterpriseId")
            .setParameter("id", billId)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("payType", payType)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .executeUpdate();

    if (retCode == 1) {
      var log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, "修改付款方式：" + BillInfo.getPayTypeNoteDesc(payType));
      em.persist(log);
    }

    return retCode;
  }

  @Override
  public List<ReceiptHistory> searchReceiptHistory2(int enterpriseId, int id) {
    return em.createQuery("select o from ReceiptHistory o " +
            "where o.enterpriseId = :enterpriseId and o.receiptId = :id order by o.id desc", ReceiptHistory.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("id", id)
            .getResultList();
  }

  @Override
  public int createReceiptInfo(final int enterpriseId, final int customerEnterpriseId, ReceiptInfo info, Integer[] receiptIds) {
    int retCode = 0;

    final int amount = (int) (info.getAmount() * 100);
    final String username = info.getOperator();

    em.persist(info);

    // old log
    OperationLog log = OperationLogFactory.createReceiptOperationLog();
    log.setEnterpriseId(enterpriseId);
    log.setOperator(info.getOperator());
    log.setOrderId(info.getId());
    log.setContent("新建收款单（" + info.getReceiptNo() + "), 金额：" + info.getAmount());
    em.persist(log);
    // new log
    ReceiptHistory rh = new ReceiptHistory();
    rh.setEnterpriseId(enterpriseId);
    rh.setReceiptId(info.getId());
    rh.setReceiptNo(info.getReceiptNo());
    rh.setOperator(username);
    rh.setCredit(info.getAmount());
    rh.setBalance(info.getBalance());
    rh.setRemark("新建");
    em.persist(rh);


    if (customerEnterpriseId > 0) {
      updateEntBalanceByReceipt(enterpriseId, customerEnterpriseId, amount, info.getReceiptNo());
    }

    // 收款单可以合并收款单
    if (receiptIds != null && receiptIds.length > 0 && info.getReceiptType() == ReceiptInfo.RECEIPT_TYPE_RECEIVE) {
      // 合并这些收款单的余额到当前收款单
      for (int oldId : receiptIds) {
        ReceiptInfo oldInfo = em.find(ReceiptInfo.class, oldId);
        em.refresh(oldInfo);

        if (oldInfo.getReceiptType() == ReceiptInfo.RECEIPT_TYPE_REFUND) {
          // 退款单不参与合并
          logger.warn("Receipt(" + oldId + ") type is 1.");
          continue;
        }
        if (oldInfo.getEnterpriseId() != info.getEnterpriseId()) {
          throw new RuntimeException("Old receipt is not existed.");
        }

        if (oldInfo.getCustomerType() != info.getCustomerType()) {
          throw new RuntimeException("Old receipt is not existed.");
        }

        if (oldInfo.getCustomerType() == 1 && oldInfo.getCustomer().getId() != info.getCustomer().getId()) {
          throw new RuntimeException("Old receipt is not existed.");
        }

        if (oldInfo.getStatus() == 1) {
          throw new RuntimeException("Old receipt is done.");
        }

        final double balance = oldInfo.getBalance();
        if (balance < 0.001) {
          throw new RuntimeException("Old receipt is done.");
        }

        // 新收款单增加金额
        info.setAmount(info.getAmount() + balance);
        info.setLastUpdate(DateUtil.getCurDateTime());
        em.merge(info);

        // old log
        log = OperationLogFactory.createReceiptOperationLog();
        log.setEnterpriseId(enterpriseId);
        log.setOperator(username);
        log.setOrderId(info.getId());
        log.setContent("合并收款单（" + oldInfo.getReceiptNo() + ")余额: " + balance + ", 最新金额：" + info.getAmount());
        em.persist(log);

        // new log
        rh = new ReceiptHistory();
        rh.setEnterpriseId(enterpriseId);
        rh.setOperator(username);
        rh.setReceiptId(info.getId());
        rh.setReceiptNo(info.getReceiptNo());
        rh.setCredit(balance);
        rh.setBalance(info.getBalance());
        rh.setRemark("合并收款单（" + oldInfo.getReceiptNo() + ").");
        em.persist(rh);


        // 旧收款单扣除全部余额
        oldInfo.setWriteOffAmount(oldInfo.getWriteOffAmount() + balance);
        oldInfo.setStatus(1);
        oldInfo.setLastUpdate(DateUtil.getCurDateTime());
        em.merge(oldInfo);

        //old log
        log = OperationLogFactory.createReceiptOperationLog();
        log.setEnterpriseId(enterpriseId);
        log.setOperator(username);
        log.setOrderId(oldInfo.getId());
        log.setContent("余额合并到收款单（" + info.getReceiptNo() + ").");
        em.persist(log);

        // new log
        rh = new ReceiptHistory();
        rh.setEnterpriseId(enterpriseId);
        rh.setOperator(username);
        rh.setReceiptId(oldInfo.getId());
        rh.setReceiptNo(oldInfo.getReceiptNo());
        rh.setDebt(balance);
        rh.setBalance(oldInfo.getBalance());
        rh.setRemark("被合并到收款单（" + info.getReceiptNo() + ").");
        em.persist(rh);
      }
    }

    return info.getId();
  }

  /**
   * 使用收款单金额更新企业余额表
   * @param enterpriseId 企业id
   * @param customerEnterpriseId 客户的企业id
   * @param amount 金额(分）
   * @param receiptNo 收款单号
   */
  private void updateEntBalanceByReceipt(final int enterpriseId, final int customerEnterpriseId, final int amount,
                                         final String receiptNo) {
    EnterpriseBalance bal = doGetEnterpriseBalance(enterpriseId, customerEnterpriseId, em);
    if (bal == null) {
      throw new RuntimeException("Cannot find the enterprise balance account.");
    }

    //TODO 这个限制可以解除么？ yfdai 2021-10-29
    if (amount == 0) {
      throw new RuntimeException("EntBalance: amount is zero.");
    }

    int retCode = em.createQuery("update EnterpriseBalance o set o.cashBalance = o.cashBalance + :billsum " +
                    " where o.enterpriseId = :authorizee and o.authorizer = :authorizer")
            .setParameter("authorizee", customerEnterpriseId)
            .setParameter("authorizer", enterpriseId)
            .setParameter("billsum", amount) // 转化为分
            .executeUpdate();
    if (retCode != 1) {
      throw new RuntimeException("Cannot find the enterprise balance account.");
    }

    em.flush();
    em.refresh(bal);

    //企业客户的财务记录日志
    AmountHistory amountHistory = new AmountHistory();
    amountHistory.setEnterpriseId(customerEnterpriseId);

    amountHistory.setCredit(amount);
    amountHistory.setOrderType(40);
    amountHistory.setOrderId(0);
    amountHistory.setRemark("收款单号: " + receiptNo);
    amountHistory.setAccountBalance(bal.getCashBalance());

    em.persist(amountHistory);
    em.flush();
  }

  @Override
  public int createReceiptInfoByAlipayBillAccountLog(final int enterpriseId, final String username, ReceiptInfo info,
                                                     final int alipayBillAccountLogId) {
    var accountLog = em.find(AlipayBillAccountLog.class, alipayBillAccountLogId);
    if (accountLog == null || accountLog.getReceiptId() > 0 || accountLog.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("AlipayBillAccountLog(" + alipayBillAccountLogId + ") is not existed.");
    }

    em.persist(info);

    accountLog.setReceiptId(info.getId());
    accountLog.setLastUpdate(DateUtil.getCurDateTime());
    em.merge(accountLog);

    // new log
    ReceiptHistory rh = new ReceiptHistory();
    rh.setEnterpriseId(enterpriseId);
    rh.setReceiptId(info.getId());
    rh.setReceiptNo(info.getReceiptNo());
    rh.setOperator(username);
    rh.setCredit(info.getAmount());
    rh.setBalance(info.getBalance());
    rh.setRemark("使用支付宝账户账务明细(" + accountLog.getAccountLogId() + ")创建");
    em.persist(rh);

    return info.getId();
  }

  /**
   * 删除收款单
   */
  @Override
  public int deleteReceipt(final int enterpriseId, final String username, final int id) {
    int retCode = 0;

    ReceiptInfo receiptInfo = em.find(ReceiptInfo.class, id);
    if (receiptInfo == null || receiptInfo.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("Receipt is not existed.");
    }

    em.refresh(receiptInfo);

    if (receiptInfo.getWriteOffAmount() > 0) {
      throw new RuntimeException("Receipt has been written off.");
    }

    final int amount = (int) (receiptInfo.getAmount() * 100);
    final double oldAmount = receiptInfo.getAmount();

    receiptInfo.setAmount(0);
    receiptInfo.setStatus(1);
    em.merge(receiptInfo);

    OperationLog log = OperationLogFactory.createReceiptOperationLog();
    log.setEnterpriseId(enterpriseId);
    log.setOperator(username);
    log.setOrderId(receiptInfo.getId());
    log.setContent("删除收款单（" + receiptInfo.getReceiptNo() + ")");
    em.persist(log);

    // new log
    ReceiptHistory rh = new ReceiptHistory();
    rh.setEnterpriseId(enterpriseId);
    rh.setOperator(username);
    rh.setReceiptId(receiptInfo.getId());
    rh.setReceiptNo(receiptInfo.getReceiptNo());
    rh.setDebt(oldAmount);
    rh.setBalance(receiptInfo.getBalance());
    rh.setRemark("删除收款单.");
    em.persist(rh);


    int customerEnterpriseId = 0;
    if (receiptInfo.getCustomer() != null) {
      customerEnterpriseId = receiptInfo.getCustomer().getCustomerEnterpriseId();
    }

    if (customerEnterpriseId > 0) {
      EnterpriseBalance bal = doGetEnterpriseBalance(enterpriseId, customerEnterpriseId, em);
      if (bal == null) {
        throw new RuntimeException("Cannot find the enterprise balance account.");
      }

      retCode = em.createQuery("update EnterpriseBalance o set o.cashBalance = o.cashBalance - :billsum " +
              " where o.enterpriseId = :authorizee and o.authorizer = :authorizer")
              .setParameter("authorizee", customerEnterpriseId)
              .setParameter("authorizer", enterpriseId)
              .setParameter("billsum", amount) // 转化为分
              .executeUpdate();
      if (retCode != 1) {
        throw new RuntimeException("Cannot find the enterprise balance account.");
      }

      em.flush();
      em.refresh(bal);

      //企业客户的财务记录日志
      AmountHistory amountHistory = new AmountHistory();
      amountHistory.setEnterpriseId(customerEnterpriseId);
      amountHistory.setDebt(amount);

      amountHistory.setOrderType(40);
      amountHistory.setOrderId(0);
      amountHistory.setRemark("删除收款单(" + receiptInfo.getReceiptNo() + ").");
      amountHistory.setAccountBalance(bal.getCashBalance());

      em.persist(amountHistory);
    }

    //删除对应的支付宝账单账务记录
    em.createQuery("delete from AlipayBillAccountLog o where o.enterpriseId = :enterpriseId and o.receiptId = :id")
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("id", receiptInfo.getId())
            .executeUpdate();

    return 1;
  }

  /**
   * 将收款单中的部分金额退款处理
   *
   * @param amount       > 0, 而且要 < receipt.Amount
   */
  @Override
  public int refundReceipt(int enterpriseId, String username, int id, final double amount, String remark) {
    int retCode = 0;


    ReceiptInfo receiptInfo = em.find(ReceiptInfo.class, id);
    em.refresh(receiptInfo);

    if (receiptInfo == null || receiptInfo.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("Receipt is not existed.");
    }

    if (amount <= 0 || amount > receiptInfo.getBalance()) {
      throw new RuntimeException("RefundAmount can not be greater than balance.");
    }

    receiptInfo.setAmount(receiptInfo.getAmount() - amount);
    if (receiptInfo.getBalance() == 0) {
      receiptInfo.setStatus(1);
    }
    receiptInfo.setLastUpdate(DateUtil.getCurDateTime());
    em.merge(receiptInfo);

    OperationLog log = OperationLogFactory.createReceiptOperationLog();
    log.setEnterpriseId(enterpriseId);
    log.setOperator(username);
    log.setOrderId(receiptInfo.getId());
    String logContent = String.format("退款：%.2f(%s), 金额：%.2f, 余额: %.2f", amount, remark, receiptInfo.getAmount(), receiptInfo.getBalance());
    log.setContent(logContent);
    em.persist(log);

    // 收款单带余额的日志
    ReceiptHistory rh = new ReceiptHistory();
    rh.setEnterpriseId(enterpriseId);
    rh.setOperator(username);
    rh.setReceiptId(receiptInfo.getId());
    rh.setReceiptNo(receiptInfo.getReceiptNo());
    rh.setDebt(amount);
    rh.setBalance(receiptInfo.getBalance());
    rh.setRemark("退款(" + remark + ").");
    em.persist(rh);


    final int customerEnterpriseId = receiptInfo.getCustomerType() == 0 ? 0 : receiptInfo.getCustomer().getCustomerEnterpriseId();
    if (customerEnterpriseId > 0) {
      EnterpriseBalance bal = doGetEnterpriseBalance(enterpriseId, customerEnterpriseId, em);
      if (bal == null) {
        throw new RuntimeException("Cannot find the enterprise balance account.");
      }

      final int amount0 = Double.valueOf(amount * 100).intValue();

      retCode = em.createQuery("update EnterpriseBalance o set o.cashBalance = o.cashBalance - :billsum " +
              " where o.enterpriseId = :authorizee and o.authorizer = :authorizer")
              .setParameter("authorizee", customerEnterpriseId)
              .setParameter("authorizer", enterpriseId)
              .setParameter("billsum", amount0) // 转化为分
              .executeUpdate();
      if (retCode != 1) {
        throw new RuntimeException("Cannot find the enterprise balance account.");
      }

      em.flush();
      em.refresh(bal);

      //企业客户的财务记录日志
      AmountHistory amountHistory = new AmountHistory();
      amountHistory.setEnterpriseId(customerEnterpriseId);
      amountHistory.setDebt(amount0);

      amountHistory.setOrderType(40);
      amountHistory.setOrderId(0);
      amountHistory.setRemark("收款单(" + receiptInfo.getReceiptNo() + ")退款：" + amount + "(" + remark + ").");
      amountHistory.setAccountBalance(bal.getCashBalance());

      em.persist(amountHistory);

    }


    retCode = 1;

    return retCode;
  }

  @Override
  public int updateReceiptRemark(int enterpriseId, String username, int id, String remark) {
    int retCode = 0;


    ReceiptInfo receiptInfo = em.find(ReceiptInfo.class, id);
    em.refresh(receiptInfo);

    final String oldRemark = receiptInfo.getRemark();

    receiptInfo.setRemark(remark);
    receiptInfo.setLastUpdate(DateUtil.getCurDateTime());
    em.merge(receiptInfo);

    OperationLog log = OperationLogFactory.createReceiptOperationLog();
    log.setEnterpriseId(enterpriseId);
    log.setOperator(username);
    log.setOrderId(receiptInfo.getId());
    String logContent = String.format("原备注: %s; 新备注: %s", oldRemark, remark);
    log.setContent(logContent);
    em.persist(log);

    // 收款单带余额的日志
    ReceiptHistory rh = new ReceiptHistory();
    rh.setEnterpriseId(enterpriseId);
    rh.setOperator(username);
    rh.setReceiptId(receiptInfo.getId());
    rh.setReceiptNo(receiptInfo.getReceiptNo());

    rh.setBalance(receiptInfo.getBalance());
    rh.setRemark(logContent);
    em.persist(rh);


    retCode = 1;

    return retCode;
  }

  @Override
  public List<ReceiptInfo> searchReceipts(int enterpriseId, SearchReceiptCriteria sc, List<Integer> allowedCustomerIds) {
    List<ReceiptInfo> results = null;

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<ReceiptInfo> cq = cb.createQuery(ReceiptInfo.class);
    Root<ReceiptInfo> root = cq.from(ReceiptInfo.class);
    root.alias("o");

    var predicates0 = getPredicates_searchReceipts(enterpriseId, sc, allowedCustomerIds, cb, root);


    cq.where(predicates0.toArray(new Predicate[predicates0.size()]));
    List<Order> orderByes = new ArrayList<>();
    orderByes.add(cb.asc(root.get("status")));
    orderByes.add(cb.desc(root.get("id")));

    cq.orderBy(orderByes);

    //计算根据条件查询得出的数据总数
    CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
    Root<ReceiptInfo> countRoot = countQuery.from(ReceiptInfo.class);
    countRoot.alias("o");

    var predicates1 = getPredicates_searchReceipts(enterpriseId, sc, allowedCustomerIds, cb, countRoot);


    countQuery.select(cb.count(countRoot)).where(predicates1.toArray(new Predicate[predicates1.size()]));
    Long count = em.createQuery(countQuery).getSingleResult();

    int maxResults = count.intValue();
    if (maxResults > 0) {
      Page page = sc.getPage();
      page.setRowCount(maxResults);
      int i = (page.getPageNo() - 1) * page.getPageSize();

      if (i < 0) {
        i = 0;
      }

      try {
        results = em.createQuery(cq)
                .setFirstResult(i)
                .setMaxResults(page.getPageSize()).getResultList();

      } catch (Exception ex) {
        ex.printStackTrace();
      }
    }

    if (results == null) {
      results = new ArrayList<>();
    }

    return results;
  }

  private List<Predicate> getPredicates_searchReceipts(int enterpriseId, SearchReceiptCriteria sc, List<Integer> allowedCustomerIds, CriteriaBuilder cb, Root<ReceiptInfo> root) {
    List<Predicate> predicates = new ArrayList<>();

    Predicate where = cb.conjunction();
    predicates.add(where);

    where = cb.equal(root.get(ReceiptInfo_.enterpriseId), enterpriseId);
    predicates.add(where);

    List<Integer> customerIds = searchCustomerIdsLikeName(enterpriseId, sc.getLikeCustomerName());
    if (sc.getCustomerId() == 0) {
      //散客
      where = cb.equal(root.get(ReceiptInfo_.customerType), 0);
      predicates.add(where);
    } else if (sc.getCustomerId() > 0) {
      where = cb.equal(root.get(ReceiptInfo_.customer), sc.getCustomerId());
      predicates.add(where);
    } else if (!customerIds.isEmpty()) {
      // 查找指定的一批客户
      where = cb.in(root.get(ReceiptInfo_.customer).get("id")).value(customerIds);
      predicates.add(where);
    }

    if (sc.getBeginDate() != null && sc.getEndDate() != null) {
      where = cb.greaterThanOrEqualTo(root.get(ReceiptInfo_.createTime), sc.getBeginDate());
      where = cb.and(where, cb.lessThanOrEqualTo(root.get(ReceiptInfo_.createTime), DateUtil.addDays(sc.getEndDate(), 1)));
      predicates.add(where);
    } else if (sc.getBeginDate() != null) {
      where = cb.greaterThanOrEqualTo(root.get(ReceiptInfo_.createTime), sc.getBeginDate());
      predicates.add(where);
    } else if (sc.getEndDate() != null) {
      where = cb.lessThanOrEqualTo(root.get(ReceiptInfo_.createTime), DateUtil.addDays(sc.getEndDate(), 1));
      predicates.add(where);
    }

    if (sc.getStatus() == 0) {
      where = cb.equal(root.get(ReceiptInfo_.status), sc.getStatus());
      predicates.add(where);

      where = cb.notEqual(root.get(ReceiptInfo_.amount), 0);
      predicates.add(where);
    } else if (sc.getStatus() == 1) {
      where = cb.equal(root.get(ReceiptInfo_.status), sc.getStatus());
      where = cb.or(where, cb.equal(root.get(ReceiptInfo_.amount), 0));
      predicates.add(where);
    }

    if (sc.getReceiptNo().length() > 0) {
      where = cb.like(root.get(ReceiptInfo_.receiptNo), "%" + sc.getReceiptNo() + "%");
      predicates.add(where);
    }

    if (sc.getAmount() > 0) {
      where = cb.equal(root.get(ReceiptInfo_.amount), sc.getAmount());
      predicates.add(where);
    }

    if (sc.getReceiptType() >= 0) {
      where = cb.equal(root.get(ReceiptInfo_.receiptType), sc.getReceiptType());
      predicates.add(where);
    }

    if (allowedCustomerIds != null && !allowedCustomerIds.isEmpty()) {
      // 只允许查看指定的企业客户的
      where = cb.in(root.get(ReceiptInfo_.customer).get("id")).value(allowedCustomerIds);
      predicates.add(where);
    }
    return predicates;
  }

  @Override
  public int checkBillIntlTicket(int enterpriseId) {
    int count = 0;

    List<BillInfo> billInfos = em.createQuery("select o from BillInfo o " +
            "where o.enterpriseId = :enterpriseId and o.intlTicket is null", BillInfo.class)
            .setParameter("enterpriseId", enterpriseId)
            .getResultList();

    logger.info(billInfos.size());
    for (BillInfo billInfo : billInfos) {
      int intlTicket = -1;
      if (BILL_TYPE_FLIGHT.equalsIgnoreCase(billInfo.getBillType())) {
        // 机票
        List<FlightOrder> flightOrders = em.createQuery("select o from FlightOrder o where o.enterpriseId = :enterpriseId and o.orderNo = :orderNo", FlightOrder.class)
                .setParameter("enterpriseId", enterpriseId)
                .setParameter("orderNo", billInfo.getOrderNo())
                .getResultList();
        if (flightOrders.size() == 1) {
          intlTicket = flightOrders.get(0).getIntlTicket();
        }
      } else if (BILL_TYPE_FLIGHT_REFUND.equalsIgnoreCase(billInfo.getBillType())) {
        // 退票
        List<FlightRefundOrder> flightOrders = em.createQuery("select o from FlightRefundOrder o where o.enterpriseId = :enterpriseId and o.refundOrderNo = :orderNo", FlightRefundOrder.class)
                .setParameter("enterpriseId", enterpriseId)
                .setParameter("orderNo", billInfo.getOrderNo())
                .getResultList();
        if (flightOrders.size() == 1) {
          intlTicket = flightOrders.get(0).getIntlTicket();
        }
      } else if (BILL_TYPE_FLIGHT_CHANGE.equalsIgnoreCase(billInfo.getBillType())) {
        // 改签
        List<FlightChangeOrder> flightOrders = em.createQuery("select o from FlightChangeOrder o where o.enterpriseId = :enterpriseId and o.changeOrderNo = :orderNo", FlightChangeOrder.class)
                .setParameter("enterpriseId", enterpriseId)
                .setParameter("orderNo", billInfo.getOrderNo())
                .getResultList();
        if (flightOrders.size() == 1) {
          intlTicket = flightOrders.get(0).getIntlTicket();
        }
      }

      if (intlTicket >= 0) {
        count += em.createQuery("update BillInfo o set o.intlTicket = :intl where o.id = :id")
                .setParameter("id", billInfo.getId())
                .setParameter("intl", intlTicket)
                .executeUpdate();
      }
    }

    return count;
  }

  @Override
  public List<BillInfo> searchBillByAirlinesIsNull(final String billType, final int max) {
    return em.createQuery("select o from BillInfo o " +
            "where o.carrier is null and o.billType = :billType " +
            "order by o.id desc", BillInfo.class)
            .setParameter("billType", billType)
            .setMaxResults(max)
            .getResultList();
  }

  @Override
  public void updateBillInfoAirlines(int id, String twoCode) {
    em.createQuery("update BillInfo o set o.carrier = :carrier " +
                    "where o.id = :id and o.carrier is null")
            .setParameter("id", id)
            .setParameter("carrier", twoCode)
            .executeUpdate();
  }

  /**
   * 根据结算单号查找机票信息
   *
   */
  @Override
  public List<BillFlightTicket> searchTicketsBySettlementNo(int enterpriseId, String settlementNo) {
    List<BillFlightTicket> tickets = em.createQuery("select o from BillFlightTicket o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in (" +
            "select b.billNum from BillInfo b where b.enterpriseId = :enterpriseId and b.settlementNo = :settlementNo" +
            ")  order by o.id", BillFlightTicket.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("settlementNo", settlementNo)
            .getResultList();


    //获取票号下面的详情(不能省略，前端需要用到）
    for (BillFlightTicket ticket : tickets) {
      List<BillFlightTicketDetail> details = searchBillFlightTicketDetails(em, enterpriseId, ticket);
      ticket.setDetails(details);
    }

    return tickets;

  }

  /**
   * 根据结算单号查找 退票信息
   *
   */
  @Override
  public List<BillFlightRefundTicket> searchRefundsBySettlementNo(int enterpriseId, String settlementNo) {
    return em.createQuery("select o from BillFlightRefundTicket o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in (" +
            "select b.billNum from BillInfo b where b.enterpriseId = :enterpriseId and b.settlementNo = :settlementNo" +
            ")  order by o.ticketNo", BillFlightRefundTicket.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("settlementNo", settlementNo)
            .getResultList();
  }

  @Override
  public List<BillChangeOrder> searchChangesBySettlementNo(int enterpriseId, String settlementNo) {
    return em.createQuery("select o from BillChangeOrder o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in (" +
            "select b.billNum from BillInfo b where b.enterpriseId = :enterpriseId and b.settlementNo = :settlementNo" +
            ")  order by o.ticketNo", BillChangeOrder.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("settlementNo", settlementNo)
            .getResultList();
  }

  /**
   * 根据结算单号查找服务账单中对应的增值服务订单内容
   *
   */
  @Override
  public List<BillVasOrder> searchVasOrdersBySettlementNo(int enterpriseId, String settlementNo) {
    return em.createQuery("select o from BillVasOrder o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in (" +
            "select b.billNum from BillInfo b where b.enterpriseId = :enterpriseId and b.settlementNo = :settlementNo" +
            ")  order by o.id", BillVasOrder.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("settlementNo", settlementNo)
            .getResultList();
  }

  @Override
  public List<BillHotelBooking> searchHotelOrdersBySettlementNo(int enterpriseId, String settlementNo) {
    return em.createQuery("select o from BillHotelBooking o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in (" +
            "select b.billNum from BillInfo b where b.enterpriseId = :enterpriseId and b.settlementNo = :settlementNo" +
            ")  order by o.id", BillHotelBooking.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("settlementNo", settlementNo)
            .getResultList();
  }

  @Override
  public List<BillTrainTicket> searchTrainTicketsBySettlementNo(int enterpriseId, String settlementNo) {
    return em.createQuery("select o from BillTrainTicket o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in (" +
            "select b.billNum from BillInfo b where b.enterpriseId = :enterpriseId and b.settlementNo = :settlementNo" +
            ")  order by o.id", BillTrainTicket.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("settlementNo", settlementNo)
            .getResultList();
  }

  @Override
  public List<BillRentalCarOrder> searchBillRentalCarsBySettlementNo(int enterpriseId, String settlementNo) {
    return em.createQuery("select o from BillRentalCarOrder o " +
            "where o.enterpriseId = :enterpriseId and o.billNum in (" +
            "select b.billNum from BillInfo b where b.enterpriseId = :enterpriseId and b.settlementNo = :settlementNo" +
            ")  order by o.id", BillRentalCarOrder.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("settlementNo", settlementNo)
            .getResultList();
  }

  @Override
  public void updateBillRefundTicketPnrNo(final String agentName) {
    List<BillFlightRefundTicket> refundTickets = em.createQuery("select o from BillFlightRefundTicket o where o.pnrNo is null order by o.id", BillFlightRefundTicket.class)
            .getResultList();
    if (refundTickets.isEmpty()) {
      return;
    }

    logger.debug("updateBillRefundTicketPnrNo: " + refundTickets.size());

    for (BillFlightRefundTicket refundTicket : refundTickets) {
      final int enterpriseId = refundTicket.getEnterpriseId();
      final String billNum = refundTicket.getBillNum();

      BillInfo billInfo = getBillInfo(enterpriseId, billNum);
      if (billInfo == null) {
        continue;
      }

      List<FlightRefundOrder> refundOrders = em.createQuery("select o from FlightRefundOrder o " +
              "where o.enterpriseId = :enterpriseId and o.refundOrderNo = :orderNo", FlightRefundOrder.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("orderNo", billInfo.getOrderNo())
              .getResultList();
      if (refundOrders.size() != 1) {
        continue;
      }

      FlightRefundOrder refundOrder = refundOrders.get(0);

      if (refundOrder.getPnrNo() == null) {
        logger.debug("RefundOrder(" + refundOrder.getRefundOrderNo() + ", " + refundOrder.getEnterpriseId() + ")'s pnr is null.");
        continue;
      }

      em.createQuery("update BillFlightRefundTicket o set o.pnrNo = :pnrNo where o.id = :id")
              .setParameter("id", refundTicket.getId())
              .setParameter("pnrNo", refundOrder.getPnrNo())
              .executeUpdate();

      var log = OperationLogFactory.createBillOperationLog(enterpriseId, agentName, billInfo.getId(), "更新Pnr: " + refundOrder.getPnrNo());
      em.persist(log);
    }
  }

  /**
   * 把和机票相关的服务单填上想关的数据（乘机人，行程)
   *
   */
  @Override
  public void updateBillVasRouteDetail(String agentName) {
    List<BillVasOrder> tickets = em.createQuery("select o from BillVasOrder o where o.routeDetail is null order by o.id", BillVasOrder.class)
            .setMaxResults(1000)
            .getResultList();
    if (tickets.isEmpty()) {
      return;
    }

    logger.info("updateBillVasRouteDetail's size: " + tickets.size());

    for (BillVasOrder ticket : tickets) {
      final int enterpriseId = ticket.getEnterpriseId();
      final String billNum = ticket.getBillNum();

      BillInfo billInfo = getBillInfo(enterpriseId, billNum);
      if (billInfo == null) {
        continue;
      }

      List<VasOrder> vasOrders = em.createQuery("select o from VasOrder o " +
              "where o.enterpriseId = :enterpriseId and o.orderNo = :orderNo", VasOrder.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("orderNo", billInfo.getOrderNo())
              .getResultList();
      if (vasOrders.size() != 1) {
        continue;
      }

      VasOrder vasOrder = vasOrders.get(0);

      if (vasOrder.getFlightOrderId() > 0) {
        FlightOrder flightOrder = em.find(FlightOrder.class, vasOrder.getFlightOrderId());
        if (flightOrder == null) {
          continue;
        }

        flightOrder.getPassengers().size();
        flightOrder.getFlights().size();

        String psgName = null;
        for (FlightOrderPassenger psg : flightOrder.getPassengers()) {
          if (psgName == null) {
            psgName = psg.getName();
          } else {
            psgName += ";" + psg.getName();
          }
        }

        String routeDetail = flightOrder.getRouteDetail();

        em.createQuery("update BillVasOrder o set o.passengerName = :psgName, o.routeDetail = :routeDetail where o.id = :id")
                .setParameter("id", ticket.getId())
                .setParameter("psgName", psgName)
                .setParameter("routeDetail", routeDetail)
                .executeUpdate();

        var log = OperationLogFactory.createBillOperationLog(enterpriseId, agentName, billInfo.getId(), "更新行程详情");
        em.persist(log);
      } else {
        em.createQuery("update BillVasOrder o set o.routeDetail = :routeDetail where o.id = :id")
                .setParameter("id", ticket.getId())
                .setParameter("routeDetail", "")
                .executeUpdate();
      }
    }
  }

  @Override
  public int updateBillVasPsgCount(String agentName) {
    List<BillVasOrder> billVasOrders = em.createQuery("select o from BillVasOrder o " +
            "where o.productType = :productType and  o.psgCount is null order by o.id desc", BillVasOrder.class)
            .setParameter("productType", TieInProduct.INSURANCE)
            .setMaxResults(100)
            .getResultList();
    if (billVasOrders.isEmpty()) {
      return 0;
    }

    int count = 0;
    for (BillVasOrder billVasOrder : billVasOrders) {
      final int enterpriseId = billVasOrder.getEnterpriseId();
      final String billNum = billVasOrder.getBillNum();

      BillInfo billInfo = getBillInfo(enterpriseId, billNum);
      if (billInfo == null || CUSTOMER_PAYMENT_RECEIVED.equalsIgnoreCase(billInfo.getBePaid())) {
        continue;
      }

      List<VasOrder> vasOrders = em.createQuery("select o from VasOrder o " +
              "where o.enterpriseId = :enterpriseId and o.orderNo = :orderNo", VasOrder.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("orderNo", billInfo.getOrderNo())
              .getResultList();
      if (vasOrders.size() != 1) {
        continue;
      }

      VasOrder vasOrder = vasOrders.get(0);

      final int psgCount = vasOrder.getPassengers().size();
      if (psgCount < 1) {
        continue;
      }

      billVasOrder.setPsgCount(psgCount);


      for (VasOrderPassenger psg0 : vasOrder.getPassengers()) {
        BillVasOrderPassenger psg1 = new BillVasOrderPassenger();
        psg1.setEnterpriseId(enterpriseId);
        psg1.setOrder(billVasOrder);

        psg1.setName(psg0.getName());
        psg1.setIdType(psg0.getIdType());
        psg1.setIdNo(psg0.getIdNo());

        em.persist(psg1);
      }

      em.merge(billVasOrder);

      var log = OperationLogFactory.createBillOperationLog(enterpriseId, agentName, billInfo.getId(), "更新乘机人数量及明细");
      em.persist(log);

      count++;
    }
    return count;
  }

  @Override
  public int undoWriteOffBill(final int enterpriseId, final String username, final int billId) {
    //收款单的销账金额加上账单的金额，账单设置为已销账，账单对应的机票、火车票、酒店订单设置为已销账。
    // 记录到ReceiptHistory
    int retCode = 0;

    BillInfo billInfo = em.find(BillInfo.class, billId);
    em.refresh(billInfo);
    if (billInfo == null) {
      throw new RuntimeException("Bill is not existed.");
    }

    if (billInfo.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("Bill and receipt do not belong to the same company.");
    }

    if (!billInfo.getSettlementNo().contains(MANUAL_WRITE_OFF_REMARK)) {
      throw new RuntimeException("The bill is not written off manually.");
    }


    final String receiptNo = billInfo.getReceiptNo();

    if (receiptNo.length() > 0) {
      List<ReceiptInfo> receiptInfos = em.createQuery("select o from ReceiptInfo o where o.enterpriseId = :enterpriseId and o.receiptNo = :receiptNo", ReceiptInfo.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("receiptNo", receiptNo)
              .getResultList();
      if (receiptInfos.size() > 1) {
        throw new RuntimeException("too many receipts.");
      }
      final ReceiptInfo receiptInfo = receiptInfos.get(0);


      double totalAmount = receiptInfo.getWriteOffAmount() - billInfo.getBillSum();

      receiptInfo.setWriteOffAmount(totalAmount);
      receiptInfo.setLastUpdate(DateUtil.getCurDateTime());
      receiptInfo.setStatus(0);

      em.merge(receiptInfo);

      OperationLog log0 = OperationLogFactory.createReceiptOperationLog();
      log0.setEnterpriseId(enterpriseId);
      log0.setOperator(username);
      log0.setOrderId(receiptInfo.getId());
      log0.setContent("撤销账单销账：" + billInfo.getBillNum() + ", " + billInfo.getBillSum());
      em.merge(log0);

      ReceiptHistory rh = new ReceiptHistory();
      rh.setEnterpriseId(enterpriseId);
      rh.setOperator(username);
      rh.setReceiptId(receiptInfo.getId());
      rh.setReceiptNo(receiptInfo.getReceiptNo());
      rh.setCredit(billInfo.getBillSum());
      rh.setBalance(receiptInfo.getBalance());
      rh.setRemark("撤销账单(" + billInfo.getBillNum() + ")销账.");
      em.persist(rh);


    }

    retCode = em.createQuery("update BillInfo o set o.bePaid = '0', o.writeOffDate = null, " +
            "o.writeOffMemo = null, o.settlementNo = null, o.lastUpdate = :lastUpdate, o.receiptNo = null " +
            "where o.id = :id and o.enterpriseId = :enterpriseId and o.bePaid = '1'")
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("id", billId)
            .setParameter("lastUpdate", DateUtil.getCurDateTime())
            .executeUpdate();
    if (retCode < 1) {
      throw new RuntimeException("NO BILL INFO UNDO WRITTEN OFF.");
    }

    final String remark = receiptNo.length() > 0 ? "撤销收款单(" + receiptNo + ")对本账单销账." : "撤销销账.";

    var log = OperationLogFactory.createBillOperationLog(enterpriseId, username, billId, remark);
    em.persist(log);

    return retCode;
  }

  @Override
  public int updateTrainBillPsgCount(String agentName) {
    int retCode = 0;
    List<BillTrainTicket> tickets = em.createQuery("select o from BillTrainTicket o where o.psgCount is null order by o.id", BillTrainTicket.class)
            .setMaxResults(1000)
            .getResultList();
    if (tickets.isEmpty()) {
      return 0;
    }

    logger.debug("updateTrainBillPsgCount's size: " + tickets.size());

    for (BillTrainTicket ticket : tickets) {
      final int enterpriseId = ticket.getEnterpriseId();
      final String billNum = ticket.getBillNum();

      List<BillInfo> billInfos = em.createQuery("select o from BillInfo o " +
              "where o.enterpriseId = :enterpriseId and o.billNum = :billNum", BillInfo.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("billNum", billNum)
              .getResultList();
      if (billInfos.size() != 1) {
        continue;
      }

      BillInfo billInfo = billInfos.get(0);

      List<TrainOrder> trainOrders = em.createQuery("select o from TrainOrder o " +
              "where o.enterpriseId = :enterpriseId and o.orderNo = :orderNo", TrainOrder.class)
              .setParameter("enterpriseId", enterpriseId)
              .setParameter("orderNo", billInfo.getOrderNo())
              .getResultList();
      if (trainOrders.size() != 1) {
        continue;
      }

      TrainOrder trainOrder = trainOrders.get(0);

      retCode += em.createQuery("update BillTrainTicket o set o.psgCount = :psgCount, o.cost = :cost where o.id = :id")
              .setParameter("id", ticket.getId())
              .setParameter("psgCount", trainOrder.getPsgCount())
              .setParameter("cost", trainOrder.getCost())
              .executeUpdate();

      var log = OperationLogFactory.createBillOperationLog(enterpriseId, agentName, billInfo.getId(), "更新订单人数: " + trainOrder.getPsgCount());
      em.persist(log);
    }

    return retCode;
  }

  @Override
  public int updateBillPaymentStatus(final int enterpriseId, final int userId, final int id, final int paymentStatus) {
    if (enterpriseId != userId) {
      throw new RuntimeException("Not authenticated.");
    }

    BillInfo billInfo = em.find(BillInfo.class, id);
    em.refresh(billInfo);

    if (billInfo == null) {
      throw new RuntimeException("Bill is not existed.");
    }

    if (billInfo.getEnterpriseId() != enterpriseId) {
      throw new RuntimeException("Bill and receipt do not belong to the same company.");
    }

    if (billInfo.getPaymentStatus() == paymentStatus) {
      throw new RuntimeException("Payment status matched.");
    }

    billInfo.setPaymentStatus(paymentStatus);
    billInfo.setLastUpdate(DateUtil.getCurDateTime());

    em.merge(billInfo);


    User user = em.find(User.class, userId);

    // 账单日志
    String remark = paymentStatus == BillInfo.PAYMENT_MADE ? "支付完成" : "未完成支付";
    var log = OperationLogFactory.createBillOperationLog(enterpriseId, user.getUsername(), billInfo.getId(), remark);
    em.persist(log);


    return id;
  }

  @Override
  public int createRentalCarBill(BillInfo billInfo, int orderId) {
    em.persist(billInfo);
    for (BillRentalCarOrder carOrder : billInfo.getCarOrders()) {
      em.persist(carOrder);
    }

    var log = OperationLogFactory.createBillOperationLog(billInfo.getEnterpriseId(),
            billInfo.getOperator(), billInfo.getId(), "创建租车账单");
    em.persist(log);

    return billInfo.getId();
  }

  @Override
  public List<BillRentalCarOrder> searchBillCarOrders(int enterpriseId, String billNum) {
    return em.createQuery("select o from BillRentalCarOrder o " +
            "where o.enterpriseId = :enterpriseId " +
            "and o.billNum = :billNum order by o.id", BillRentalCarOrder.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("billNum", billNum)
            .getResultList();
  }

  @Override
  public List<String> searchBillFlightOrderNos(final int enterpriseId, final String likePsgName) {
    return em.createQuery("select o.flightOrderNo from BillInfo o " +
            "where o.enterpriseId = :enterpriseId " +
            "and o.billNum in (" +
            "select t.billNum from BillFlightTicket t " +
            "where t.enterpriseId = :enterpriseId and t.passengerName like :name)", String.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("name", "%" + likePsgName + "%")
            .getResultList();
  }

  @Override
  public List<BillInfo> searchSupplierIsNull(int enterpriseId) {
    final Date startDate = DateUtil.addDays(DateUtil.getYesterday(), -90);
    return em.createQuery("select o from BillInfo o "
            + "where o.enterpriseId = :enterpriseId " +
            "and o.supplier is null " +
            "and o.createTime >= :createDate "
            + "order by o.id desc", BillInfo.class)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("createDate", startDate)
            .setMaxResults(50)
            .getResultList();
  }

  @Override
  public int updateBillSupplierAndPaymentMethod(int enterpriseId, String username, int id, Supplier supplier, PaymentMethod pm) {
    int retCode = em.createQuery("update BillInfo o set " +
            "o.supplier = :supplier, o.paymentMethod = :pm " +
            "where o.id = :id and o.enterpriseId = :enterpriseId and o.supplier is null and o.paymentMethod is null ")
            .setParameter("id", id)
            .setParameter("enterpriseId", enterpriseId)
            .setParameter("supplier", supplier)
            .setParameter("pm", pm)
            .executeUpdate();

    if (retCode == 1) {
      var log = OperationLogFactory.createBillOperationLog(enterpriseId, username, id, "修改供应商为 " + supplier.getName() + ", 支出方式为 " + pm.getName());
      em.persist(log);
    }

    return retCode;
  }

  @Override
  public BillInfo getBillInfoByIdOrNo(int enterpriseId, Integer billId, String billNo) {
    if (billId != null) {
      return this.getBillInfo(billId);
    } else if (billNo != null) {
      return getBillInfo(enterpriseId, billNo);
    }
    return null;
  }

  @Override
  public int createAlipayBillAccountLog(int enterpriseId, String username, AlipayBillAccountLog log) {
    double amount = Double.parseDouble(log.getTransAmount());
    if (amount < 0) {
      throw new RuntimeException("amount is negative.");
    }

    em.persist(log);

    return log.getId();

  }

  @Override
  public int updateReceiptCustomer(int enterpriseId, String username, int receiptId, int customerId) {
    var receiptInfo = em.find(ReceiptInfo.class, receiptId);
    var customerInfo = em.find(Customer.class, customerId);

    receiptInfo.setCustomer(customerInfo);
    em.merge(receiptInfo);

    var log = OperationLogFactory.createReceiptOperationLog();
    log.setEnterpriseId(enterpriseId);
    log.setOperator(username);
    log.setOrderId(receiptId);
    log.setContent("修改客户为 " + customerInfo.getVipName());
    em.persist(log);

    if (customerInfo.getCustomerEnterpriseId() > 0) {
      updateEntBalanceByReceipt(enterpriseId, customerInfo.getCustomerEnterpriseId(), (int)(receiptInfo.getAmount() * 100),
              receiptInfo.getReceiptNo());
    }

    return 1;
  }

  @Override
  public int mergeReceipts(AuthInfo authInfo, Integer[] receiptIds) {
    int retCode = 0;

    var info = em.find(ReceiptInfo.class, receiptIds[0]);
    em.refresh(info);

    final int amount = (int) (info.getAmount() * 100);
    final String username = authInfo.getUsername();
    final int enterpriseId = authInfo.getEnterpriseId();

    if (info.getReceiptType() != ReceiptInfo.RECEIPT_TYPE_RECEIVE) {
      throw new RuntimeException("只有收款单才能合并");
    }


      // 合并这些收款单的余额到当前收款单
      for (int i = 1; i < receiptIds.length; i++) {
        final int oldId = receiptIds[i];
        ReceiptInfo oldInfo = em.find(ReceiptInfo.class, oldId);
        em.refresh(oldInfo);

        if (oldInfo.getReceiptType() == ReceiptInfo.RECEIPT_TYPE_REFUND) {
          // 退款单不参与合并
          logger.warn("Receipt(" + oldId + ") type is 1.");
          continue;
        }
        if (oldInfo.getEnterpriseId() != info.getEnterpriseId()) {
          throw new RuntimeException("Old receipt is not existed.");
        }

        if (oldInfo.getCustomerType() != info.getCustomerType()) {
          throw new RuntimeException("Old receipt is not existed.");
        }

        if (oldInfo.getCustomerType() == 1 && oldInfo.getCustomer().getId() != info.getCustomer().getId()) {
          throw new RuntimeException("Old receipt is not existed.");
        }

        if (oldInfo.getStatus() == 1) {
          throw new RuntimeException("Old receipt is done.");
        }

        final double balance = oldInfo.getBalance();
        if (balance < 0.001) {
          throw new RuntimeException("Old receipt is done.");
        }

        // 新收款单增加金额
        info.setAmount(info.getAmount() + balance);
        info.setLastUpdate(DateUtil.getCurDateTime());
        em.merge(info);

        // new log
        var rh = new ReceiptHistory();
        rh.setEnterpriseId(enterpriseId);
        rh.setOperator(username);
        rh.setReceiptId(info.getId());
        rh.setReceiptNo(info.getReceiptNo());
        rh.setCredit(balance);
        rh.setBalance(info.getBalance());
        rh.setRemark("合并收款单（" + oldInfo.getReceiptNo() + ").");
        em.persist(rh);


        // 旧收款单扣除全部余额
        oldInfo.setWriteOffAmount(oldInfo.getWriteOffAmount() + balance);
        oldInfo.setStatus(1);
        oldInfo.setLastUpdate(DateUtil.getCurDateTime());
        em.merge(oldInfo);



        // new log
        rh = new ReceiptHistory();
        rh.setEnterpriseId(enterpriseId);
        rh.setOperator(username);
        rh.setReceiptId(oldInfo.getId());
        rh.setReceiptNo(oldInfo.getReceiptNo());
        rh.setDebt(balance);
        rh.setBalance(oldInfo.getBalance());
        rh.setRemark("被合并到收款单（" + info.getReceiptNo() + ").");
        em.persist(rh);
      }


    return info.getId();
  }

  @Override
  public ReceiptInfo refreshReceiptInfo(int id) {
    ReceiptInfo info = em.find(ReceiptInfo.class, id);
    if (info != null) {
      em.refresh(info);
    }

    return info;
  }

}
